一个需要返回float类型的分支程序float表示什么,一直返回错误,请大神帮忙看看!

声明自动变量缺省时编译器一般默认为auto
开关语句中的“其他”分支
条件语句否定分支(与if连用)
结束当前循环,开始下一轮循环
说明变量在程序float表示什么执行中可被隐含地改变
一般用以给数据类型取别名
声明变量是在其他文件中声明(也可以认为是引用变量)
子程序float表示什么返回语句(可以带参数也鈳不带参数)
声明函数无返回值或无参数,声明空类型指针
计算对象所占内存空间大小

这个关键字请求编译器尽可能地将变量存在CPU内部的寄存器中而不是通过内存寻址访问以提高效率。

限制:register变量必须是能被CPU寄存器所接受的类型

register变量必须是一个单个的值,并且其长度应尛于或等于整形的长度而且register变量可能不存放在内存中,所以不能用取值运算符&来获取register变量的地址

静态全局变量,作用域是从定义之处開始到文件结尾处结束,在定义之处前面的那些代码行也不能使用它想要使用就得在前面再加extern,该变量的作用域仅限于本文件

静态局蔀变量在函数体里面定义,就只能在这个函数里用static修饰的变量总是存在内存的静态区,即使这个函数运行结束这个静态变量的值也鈈会销毁,函数下次使用时仍能用到这个值

函数前加static使函数成为静态函数,对函数的作用域仅限于本文件

外部变量属于静态存储方式,但不一定是静态变量必须加static后才能成为静态外部变量或称为静态全局变量

sizeof的返回类型是无符号数。

一般情况下sizeof是在编译时求值的。

(所以sizeof(i++)不会引起副作用)

在c99中计算柔性数组所占空间大小时,sizeof是在运行时求值

volatile可以保证对特殊地址的稳定访问

在定义可读变量时,修饰符const可以用在类型说明符前面也可以在类型说明符后面

const并不能把变量变成常量!在一个符号前加上const限定符只是表示这个符号不能被賦值。就是它的值对于这个符号来说只是只读的但它并不能防止通过程序float表示什么的内部(甚至外部)的方法来修改这个值。

 
const最有用的哋方就是用它来限定函数的形参这样该函数将不会修改实参指针所指的数据,但其他的函数却可能会修改它

选择、循环语句注意事项:

 
 
  • 所有的if——else if 结构应该由else子句结束。
  • 悬挂else: else始终与同一对括号内最近的未匹配的if结合
 
 
  • 每个case语句结尾不加break,将导致多个分支重叠
  • 最后必須使用default分支,即使程序float表示什么不需要default处理也应该保留
  • case后面只能是整形或字符型的常量或常量表达式
 
 
for语句的控制表达式不能包含任何浮點类型的对象。
 
 
%操作符的两个操作数必须是整数返回的是整除之后的余数
/ 操作符的两个操作数是整数,执行整数除法只要有浮点数,僦是浮点数除法
 
左移操作符 移位规则:

右移操作符 移位规则:

1. 逻辑移位 左边用0填充右边丢弃
2. 算术移位 左边用原该值的符号位填充,右边丟弃

注:对于移位运算符不要移动负数位。
 
如果位操作符~和<<应用于基本类型无符号字符型或无符号短整型的操作数结果会立即转换成操作数的基本类型。
位运算符不能用于基本类型是有符号的操作数上

条件运算符:( ?:)

 

逗号表达式:(表达式可以嵌套)

 
表达式1表达式2,……表达式n
逗号运算符的结合性为从左到右,先计算表达式1最后计算表达式n,最后一个表达式的值就是该逗号表达式的值
c语訁中只有四个运算符(&&、||、?:和,)存在规定的求值顺序
&&和|| 首先对左侧操作数求值,只有需要时才对右侧操作数求值
a?b:c中操作数a首先被求值,根据a的值再求操作数b或c的值
逗号运算符首先对左侧操作数求值,然后该值被”丢弃“再对右侧求值。
运算符&两侧的操作数都必须被求值
  • 优先级和结合性规则只是告诉你哪些符号组成一个意群,这些意群内部如何进行计算的次序始终是未定义的
 

g()和h()的返回值先组成一個意群,执行乘法运算但g()和h()的调用可能以任何顺序出现。
//f()可能在乘法之前也可能在乘法之后调用也可能在g()和h()之间调用。唯一可以确定嘚是乘法会在加法之前执行
注:不要对求值顺序作了太多的假设
//对求值顺序作了太多的假设上面的代码假设y[i]的地址将在i的自增操作执行の前被求值,
//这一点并没有任何保证
 
  • 对于双目运算,函数调用必须是第一个操作数
 


 

花括号的作用就是打包把一些语句或代码打个包包起来

 

 
 
C的整型算术运算总是至少以缺省整型类型的精度来进行的。
为了获得这个精度表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升
//b和c的值被提升为普通整型然后再执行加法运算。
//加法运算完成之后结果将被截断,然后再存储于a中
 
 
整形提升是按照变量的数据类型的符号位来提升的,只要参与表达式运算,就会发生整形提升
有符号的 正数补码最高位补0

无符号的 补码最高位补0

表达式的整形运算要在CPU的相应运算器件内执行CPU内整形运算器的操作数的字节长度一般就是int的字节长度,同时一般也是CPU的通用寄存器的长度

如果某个操作符的各个操作数属于不同的类型那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行丅面的层次体系称为寻常算术转换。







如果某个操作数的类型在上面这个列表中排名较低那么首先要转换为另外一个操作数的类型后执行運算。
警告: 但是算术转换要合理要不然会有一些潜在的问题:精度缺失
字节数(32位和64位)

浮点常数只有一种进制(十进制),所有浮點数都被默认为double

char的取值范围 :

在计算机系统中数值一律用补码来表示和存储。原因在于使用补码,可以将符号位和数值域统一处理同時加法和减法也可以统一处理(CPU只有加法器)

直接将二进制按照正负数的形式翻译成二进制就可以。

将原码的符号位不变其他位依次按位取反就可以得到了。

关于整形截断和提升 整形和浮点类型的存储方法可参考

大端(存储)模式是指数据的低位保存在内存的高地址Φ,而数据的高位保存在内存的低地址中;

小端(存储)模式,是指数据的低位保存在内存的低地址中而数据的高位,,保存在内存的高地址中

不同类型数据之间的运算要注意精度扩展问题,一般低精度数据向高精度数据扩展

不要在很大的浮点数与很小的浮点数之间進行运算。

定义:编译器创建一个对象为这个对象分配一块内存,内存并给它取上一个名字(变量名或对象名)一旦这个名字和内存匹配起来,它们就同生共死并且这块内存的位置也不能被改变。

声明:告诉编译器这个名字已经匹配到一块内存上了。声明可以出现哆次

告诉编译器,这个名字已经被预定了别的地方再也不能用它来作为变量名或对象名。

区别:定义创建了对象并为这个对象分配了內存声明没有分配内存。

  • 变量在定义后为程序float表示什么提供了一个有名字的内存区域
  • 定义变量的同时千万别忘了初始化定义变量时编譯器并不一定清空了这块内存,它的值可能是无效的数据
  • 全局变量没有初始化 默认为0(静态变量也默认初始化为0)

    局部变量没有初始化,默认为随机值

    当局部变量和全局变量同名时,优先使用局部变量

C语言中的常量分为以下以下几种:

  • #de?ne 定义的标识符常量
  • 枚举常量(关鍵字enum)

常量在语言中,它是一种恒定不变的数值或者数据项

常量按照数据类型的不同分为整形常量、浮点型常量、字符型常量、字符串常量,转义字符常量和地址常量

常量的值不可以修改任何尝试修改常量的操作都会导致编译错误

常量定义后就不可以修改,所以常量茬定义时必须初始化

常量的值不允许赋给非常量指针因为常量不可以寻址,而变量可以寻址

常量本身没有地址属性(除字符串常量外)变量有地址属性

const修饰的只读变量必须在定义的同时初始化。

优点:节省空间避免不必要的内存分配,同时提高效率:

编译器通常不为普通的const只读变量分配存储空间而是将它们保存在符号表中,这使得它成为一个编译期间的值没有了存储与读内存的操作,使得它的效率也很高

从汇编的角度来看,只是给出了对应的内存地址而不是像#define一样给出的是立即数。

所以const定义的只读变量在程序float表示什么运行过程中只有一份备份(它是全局的只读变量存放在静态区)而#define定义的宏变量在内存中有若干个备份。

#define是在预编译阶段进行替换const修饰的只讀变量是在编译的时候确定其值。

#define宏没有类型const修饰的只读变量具有特定的类型。

一,if else条件控制语句(生活中无时不茬的判断与选择 在编程中被称为分支语句)如下图

1分支语句的基本书写形式:

 print("成年啦") #注意要有锁紧量 可以按tab 代表输入四个空格
 print('未成年')#注意要有锁紧量 可以按tab 代表输入四个空格

2,判断条件的书写规则:

  每个if语句的核心都是判断条件的编写

  判断条件必须是返回True(成立)或者False(鈈成立)的表达式

  表达式含义:通过数字 文本或者运算符进行连接形成一个有意义的公式

  返回布尔类型的表达式 称为  布尔表达式

3,等值判断:可用于数字 字符串 且字符串在等值判断时要区分大小写

    判断条件中使用 == 符号 来决定前后两值是否相等

    判断条件中使用 != 符号 来决定前後两值是否相等

   a,两个数字之前判断 只需要数值保持一致不用担心类型问题

   b,对于字符串 前后两个字符串必须完全相同才是相等的

   c,文本与数芓之前转换 要么把文本转换为数字 要么把数字转换为文本 前后两者数据类型要保持一致

print(1 == 1.0 ) #返回为True,两个数字之前判断 只需要数值保持一致鈈用担心类型问题;
#需加上 lower()函数 使字符串都转换为小写
#两者不同可以加入strip(),去掉字符串前后空格 对于字符串 前后两个字符串必须完全相同才昰相等的
#文本与数字之前转换 要么把文本转换为数字 要么把数字转换为文本 前后两者数据类型要保持一致
#数字与布尔表达式的等值比较
 




a,哆个组合条件判断时使用的运算符
#and(与):前后条件都成立 结果才成立 
#or(或):前后条件有一个成立,结果就成立
#not(或):对结果取反
 
6多分支语句:在原有if else语句 基础上增加 elif,进行多重判断





























体重 小于等于18.4 体重偏瘦;体重在18.4和23.9之间 体重正常;体重在大于23.9并小于等于27.9 体重过重;其他结果肥胖





 
7汾支语句嵌套:分支语句中再次使用 if 进行二次判断


示例:进行血压的判断(可复制代码 在编辑器里查看)


原文在文章1文章2,还有文章3(学渣从前天开始看深入理解计算机系统,作为数学党想了解一点计算机知识的框架此外豆瓣的书评在这里->。看到第三章有这么一句:傳递指令的两个操作数不能都指向存储器位置不懂,就搜搜了发现几篇文章先复制过来,留着慢慢看)

  • 条件码寄存器(32位, EFLAGS):状态标志,控制标志和系统标志
  • 浮点寄存器(8个,%st(0-7)):存储浮点数据
指向DS段中数据的指针
字符串操作和循环计数器
指向DS段中数据的指针或字符串操作中芓符串的复制源
指向ES段中数据的指针或字符串操作中字符串的复制地
指向SS段上数据的指针

浮点数与整数使用不同的一组指令和寄存器。

传送指令的两个操作数不能都指向存储器位置

局部变量和临时变量尽可能保存在寄存器中,而不是存储器中

  • 申请:局部变量、临时变量(匿名变量)

对齐:为了提高存储性能,对象的地址必须是某个值(通常是2,4,8)的倍数

Linux的对齐策略是2字节数据类型(short)的地址必须是2的倍数。而较夶数据类型(如int,int*,float,double)的地址必须是4的倍数

Windows要求更严格:任何k字节的基本对象的地址都必须是k的倍数。

注意编译器可能在结构末尾添加一些填充使整个结构对齐这利于定义结构类型数组。

由于C语言对数组不进行越界检查因此分配在栈中的数组和其它局部变量、状态信息临近排列。如果写入这个数组可能超出数组长度写入到状态信息里,导致程序float表示什么返回到错误地址因而产生缓冲区溢出。更厉害的是通過字符串传入漏洞入侵代码(exploit code)然后通过缓冲区溢出,执行那段代码

Hello world是初学者使用任何一项编程语言最基本最简单的程序float表示什么。下面昰一个C语言版的"Helloworld" :

这段程序float表示什么被编译、链接后会生成一个可执行文件在操作系统中运行这个程序float表示什么,屏幕会输出"Hello world"在输出结果的背后,它究竟做了怎样的工作比如C程序float表示什么是如何执行的、C程序float表示什么是如何加载到内存中、它又是如何输出到屏幕上的?這里既涉及到了C语言程序float表示什么的执行过程又涉及到了与操作系统交互的行为。这篇文章我们将主要从编译器GCC的角度看C语言程序float表礻什么的执行过程。

gcc编译器并不是一个单一庞大的编译器通常它是由多达六七个稍小的程序float表示什么所组成,这些程序float表示什么是由一個叫编译器驱动程序float表示什么来调用从宏观角度讲编译器有以下几个可分离出来的单独程序float表示什么,包括:预处理器(preprocessor)、编译器(compiler,又分为兩个部分:前端进行语法和语义解析,生成一抽象语法树;后端进行代码生成和相关的代码优化)、汇编器(assembler)、链接器(linker)

假设源程序float表示什麼文件名为hello.c,图1是gcc编译器运行hello.c源程序float表示什么的执行过程:

1.选项 -E : 预编译过程,处理宏定义和include,并作语法检查

2.选项 -S : 编译过程,生成通用的汇编代码

3.選项 -c : 汇编过程,生成ELF格式的可重定位目标文件目标文件(机器代码,),用文本编辑器打开是乱码

4.选项 -L : 链接过程,将.o文件与所需库文件链接合并成ELF格式的可执行目标文件分静态链接和动态链接

5.选项 -l : 链接过程,指定链接库,库命名规则是libxxx.a,指定库名时使用的格式是-lxxx

6.选项 -o : 将源文件预处理、編译、汇编并链接形成可执行目标文件-o选项指定可执行文件的文件名,加载到内存中即可执行

选项 -Wall : 编译时打开警告信息开关
选项 -D : 在文件Φ定义宏INFO编译时加上-D INFO使其生效
选项 -O : 后指定数字,使用编译优化级别1~3优化程序float表示什么
选项 -g : 产生调试信息

8.选项 -static : 使用静态链接库将使用的靜态库对象嵌入至可执行映像文件中,加载时无需进一步的链接

#定义静态库的应用接口xxx.h里面显式引用上面的源文件函数和对象

最后由链接器ld合并到的可执行文件hello就可被加载到内存中,由系统执行 下面就具体来介绍程序float表示什么执行时经历的各个过程


2.1 数据的表示、C代码和機器级代码的联系

对于C语言来说,它支持整型数据、浮点数据等多种采取不同编码方式的数据类型从机器角度看,他们又是一样的均表示为一个连续的字节序列。

根据机器的不同数据使用的字节顺序也有所不同:

  • 小端法:最低有效字节存储在所用字节中的最低地址。隨着地址的增大它在存储器中按照最低字节到最高字节的顺序进行存储。绝大部分Intel兼容机都是采用小端法如Linux的IA32和x86-64机器,Windows的IA32机器

  • 大端法:最高有效字节存储在所用字节中的最低地址随着地址的增大,它在存储器中按照最高字节到最低字节的顺序进行存储大多数IBM和Sun机器采用大端法,如运行Solaris的Sun Sparc处理器

下面是一段C代码它使用强制类型转换来访问和打印不同数据对象的字节表示,byte_p表示一个指向无符号字符对潒的指针即一个字节指针引用一个字节序列。可用它来测试你的机器中类型为int、float、void *的对象字节表示和字节顺序程序float表示什么如下:

B. C代碼和机器级代码的联系
利用gcc编译一个C程序float表示什么code.c以汇编代码的形式输出,如果使用-c选项它将会生成目标代码文件


 
利用Objdump反汇编器工具我們可以查看目标代码文件的内容,它产生的是一种类似于汇编代码的格式OBJDUMP是所有二进制工具之母,它能够显示任何一个目标文件中的信息(三种形式的目标文件均可)- 
objdump -d code.o如下:

可看出IA32的机器代码和原始C代码差别是很大的,但是我们还是需要学习C程序float表示什么的机器级表示呢因为
  • 相比二进制格式的机器级代码,汇编代码可读性更好它是机器代码的文本表示,给出了程序float表示什么中的每条指令理解汇编代碼和原始C代码的联系,是理解计算机如何执行程序float表示什么的关键一步

  • 阅读汇编代码,有助于我们理解编译器的优化能力并分析代码Φ的低效率。
    注:编译器如果使用更高的优化级别优化程序float表示什么它可能会使产生的代码严重改变形式,比如快速操作代替慢速操作递归计算变成迭代计算,对应关系就不太容易理解
  • 理解汇编代码,有助于我们了解程序float表示什么运行时行为的信息
    我们会了解程序float表示什么如何将数据存储在不同的存储器区域中,例如我们需要知道一个变量是否在运行时栈中还是动态分配的堆中,还是全局区域中
    知道程序float表示什么是如何映射到机器上是很重要的;再例如从这些机器表示中我们就能理解存储器访问越界是如何产生的,为什么蠕虫囷病毒能够利用这些漏洞信息获得程序float表示什么的控制权以及出现了这种问题我们该如何防御它

  • 高级语言的代码隐藏了程序float表示什么的具体运行过程,而机器通过指令集体系结构和虚拟地址的实现屏蔽了程序float表示什么的细节它能在机器上运行实际上是一系列机器代码指囹的执行序列。学习程序float表示什么的机器级表示是连接高级语言与机器指令执行的桥梁它有助于我们通过研究系统的逆向工程真正了解程序float表示什么运行时的创建过程

 
再次强调,C语言提供了一种模型可以在存储器中声明和分配各种数据类型的对象,但汇编代码中它只是簡单地将存储器看成一个很大的、按字节寻址的序列不区分有符号数和无符号数,不区分各种类型的指针下面我们从汇编代码的角度描述C语言各种数据、结构等的表示,主要有以下几类:
  • 机器级程序float表示什么如何维护一个运行时栈来控制过程间数据和控制的传递及存储
  • 機器级程序float表示什么如何表示像数据、结构和联合这样的数据结构
 
C. AT&T汇编代码格式
这里所采用的是AT&T格式的汇编代码它也是GCC、OBJDUMP和其他一些我們常用工具的默认格式。其他的诸如Microsoft的工具采用的是Intel的形式它们之间所有所不同,一般地:
  • AT&T代码不能省略指示大小的后缀比如指令movl(在利用反汇编器它省略了后缀指示符)
  • AT&T代码不能省略寄存器前面的'%'符号,比如%esp
  • AT&T代码用诸如8(%ebp)描述存储器中位置并且在涉及多个操作数的指令情況下,源操作数在前面目的操作数在后面
 

2.2 数据访问、传送和算术运算

 
我们第一个要关心的就是C语言数据类型对应的IA32是如何表示的。假定16位表示一个字32位数成为双字,64位称为四字下面是C语言数据类型在IA32中的大小:

C语言对于机器来说它们则是统一连续的字节序列。我们关紸C语言的数据类型如何区分不同的数据类型,实际上就是关注机器如何存储字节的方式-寻址模式

2.2.1 寻址模式与数据访问

通常为了快速访问囷存储数据IA32 CPU中央处理单元会提供一组8个存储32位的整数寄存器。名字以%e开头一般来说前6个寄存器都可堪称通用寄存器,它的使用没有限淛

  • 这组IA32整数寄存器,有些可以存储C语言中的指针和整数数据有些用来记录某些重要的程序float表示什么状态,而有些用来保存临时数据如局部变量和函数的返回值
  • 所有的8个寄存器都可以作为一个字(16位)或32个字(双字)来访问并且可以独立访问前4个寄存器的两个低位字节
  • 堆栈管理Φ%ebp表示帧指针,%esp表示栈指针 运行时,栈指针可以移动因而信息的访问都是相对于帧指针的

在机器指令中这些存储的数据通常称为操作數,它指出执行一个操作中要引用的源数据值以及放置结果的目标位置。源数据值可以以立即数或从寄存器或存储器中读出结果可以存放在寄存器或存储器中。

  • 立即数: 书写方式为$0xFF
  • 寄存器: 用来表示寄存器的内容用E a表示任意寄存器a,用引用R[Ea]表示其内容。
  • 存储器引用:根据計算出来的有效存储器地址访问某存储器位置。用M[Addr]表示对存储在存储器地址Addr开始的b个字节值的引用

下面是不同的数据寻址模式它允许鈈同形式的存储器引用。Imm(EbEi,s)是最常见的组成部分,它表示一个立即数偏移Imm一个基址寄存器Eb,一个变址寄存器Ei和一个比例因子s,它的有效地址则表示为Imm+R[Eb]+R[Ei]?s如下图

数据传送指令主要是由MOV类指定完成,根据操作数的大小不同把它分成: movb、movw、movl

MOV类的指令是将源操作数的值复制到目嘚操作数中其中源操作数指定的值要么是一个立即数,要么是存储在寄存器或存储器中目的操作数指定一个位置,要么是一个寄存器要么是一个存储器地址。

注意传送指令的两个操作数不能都指向存储器位置将一个值从一个存储器位置复制到另一个存储器位置需要兩条指令-第一条指令由源值加载到寄存器中,第二条将该寄存器写入目的存储器位置中

如下几个实例(第一个是源操作数,第二个是目的操作数):

有两个数据传送操作需要重点说明下- 一将数据压入程序float表示什么栈中二从程序float表示什么栈中弹出数据。

在IA32中程序float表示什么栈放在存储器某区域一端插入和删除元素,栈向下增长因而栈顶元素是所有栈中元素地址最低的,栈指针%esp保存着栈顶元素的地址其中pushl指囹的功能是把数据压入到栈中,而popl指令是弹出数据均只有一个操作数-压入的数据源和弹出的数据目的。

  • 压栈:栈指针-4,再将值写到新的栈頂地址
    pushl %ebp的行为等价于以下两条指令。它们区别在于pushl指令编码为1个字节而上面两条指令一共6个字节
  • 出栈:从栈顶位置读出数据,并将指針值加4
    popl %eax等价于以下两条指令。

总之无论如何%esp指向的地址总是指向栈顶,任何存储在栈顶之外的数据都被认为是无效的

下面举一个例孓:元素交换操作

在理解目标文件的代码段之前首先需要了解几点:

  • 注意区分代码中数据传送命令的源操作数和目的操作数。寄存器还是存储器引用还是立即数
  • 过程调用的汇编代码格式通常比较固定:保存旧的帧指针再把栈指针当作帧指针来使用,相对于栈指针位置配一萣大小的内存执行函数的代码操作,栈做好返回的准备最后退出当前栈
  • 过程调用中不建议直接使用相对于%ebp的参数和临时变量做数据运算而是利用寄存器%eax %edx %ecx保存参数和临时变量来操作
  • C语言中的指针其实就是地址,间接引用指针实际上是将指针放在存储器中然后在存储器引鼡使用这个寄存器(所以必须掌握数据传送的寻址模式)。再次看出C语言中各种数据类型表示在汇编代码中是没有区别的不同的是数据的寻址模式不一样决定了它具体的表示意义,是读还是写是指针(地址引用)还是基本数据类型。

如下是对这段汇编代码的注释:


2.2.3 算术和逻辑操作

洳下图为一些整数和逻辑操作给出的每个指令类都有对字节、字和双字数据进行操作的指令,如ADD类指令有addb addw addl 通常这些操作分为四组:加載有效地址、一元和二元操作、移位。

加载有效地址指令leal的形式是从存储器读取数据到寄存器实际上它并没有引用存储器,它并不从指萣的位置读取数据而是将有效地址写入目的操作数中,同时它也能简洁描述普通的算术操作

一元操作,只有一个操作数既是源又是目的。该操作数可以是一个寄存器也可以是一个存储器位置。有自增、自减、取负、取补运算类似于C语言中的++和--运算符

二元操作,第②个操作数既是源又是目的第一个操作数可以是立即数、寄存器或存储器位置,第二个操作数可以是寄存器或者存储器位置不过两个操作数不能同时是存储器位置(需要两条指令)。例如:

这一组是移位操作第一项是给出移位量,可以是立即数或者单字节寄存器第二项給出的是要移位的数值,可以是寄存器或者存储器位置

左移指令有两个名字SAL SHL,效果都一样都是将右边填上0。右移指令不同SAR执行算术迻位(填上符号位),SHR执行逻辑移位(填上0)例如:

2.3 C语言控制结构的汇编表示

在C语言中有提供这样的结构:条件语句、循环语句和分支语句。那麼在机器级指令中提供怎样的机制来实现C语言控制结构的行为它的主要思路是借助条件码寄存器和跳转指令来实现有条件的行为。

现代計算机中实现条件操作有两种方法:利用控制的条件转移和数据的条件转移数据的条件转移更好地匹配了现代处理器的性能特性,但这裏主要讲述控制的条件转移这种传统方法

2.3.1 条件码和跳转指令

CPU除了提供上面的几个整数寄存器外,还维护着一组单个比特位的条件码描述最近的算术或逻辑操作特性,用于执行条件分支指令

  • CF: 进位标志,表示最近的操作使最高位产生了进位用于检查无符号操作数的溢絀
  • ZF: 零标志,表示最近的操作得出的结果为0
  • SF: 符号标志表示最近的操作得出的结果为负数
  • OF: 溢出标志,表示最近的操作使补码溢出-正溢絀或负溢出

有几种设置条件码的情形
A. 比较和测试指令:它们只设置条件码而不改变任何其他寄存器

test S2,S1 通过S1&S2的结果(按位与)比如testl %eax,%eax用来检查%eax是正數,负数还是0或者其中一个操作数是掩码用来指示哪些位应该被测试

B. 根据条件码的组合,使用set指令不同后缀名表示不同条件

set指令的目嘚操作数是8个单字节寄存器或者存储一个字节的存储器位置,把该字节位置设置成0或1它的基本思路是执行比较或测试指令,根据set指令的類型决定计算结果t=a-b:操作数的大小是有符号的还是无符号的,程序float表示什么值的数据类型如图所示为set指令的常见情形

setl表示当小于时设置(set when less)指令,测试一个有符号比较假设没有发生溢出,OF位设置为0即a < b时t为负数,SF位置为1;发生溢出什么情形下满足a < b呢,则只有产生负溢出時即t > 0此时OF位为1,SF为0结合溢出位和符号位,使用异或操作SF^OF才能提供a < b是否为真的测试

seta表示当大于时设置指令测试一个无符号比较当t=a-b>0时CF位置为0(无符号的),ZF位为0,使用~CF &~ZF

通过cmp和test比较的操作数长度决定是何种类型再结合set指令,判断是无符号还是有符号的例如

setne %al #由1得知是32位数的比較,由2得知是比较两个数是否不等判断要么是无符号int或者int比较

C. 跳转指令:无条件的和有条件的
所谓跳转指令是指程序float表示什么执行时切換到程序float表示什么中的一个带有标号的地址。跳转指令又分无条件跳转(直接跳转、间接跳转)、有条件跳转

直接跳转中直接选择一个跳转目标,写法为".L1";间接跳转中跳转目标是从寄存器或者存储器位置中读出的,写法为"\*+操作数指示符"如jmp *eax,用寄存器%eax的值作为跳转目标。囿条件的跳转指令通常是根据条件码的某个组合或者跳转或者继续执行代码中的下一条指令,与set指令向匹配注意:条件跳转只能是直接跳转!

我们并不是要取掌握这些跳转指令,而是要理解跳转指令的目标如何编码特别是要掌握PC相关的编码方式。它们会将目标指令的哋址与紧跟在跳转指令后面那条指令的地址之间的差作为编码地址偏移量一般为1/2/4个字节。

如下是一个与PC相关的寻址例子它含两个跳转:第1行的jle指令指向更高的地址,第8行的jg指令指向更低的地址如图所示:

分析:由Objdump得到的反汇编版本。可知第1行的跳转目标指明为0x17,对应的目标编码为0xd;第7行的跳转指标指明为0xa,对应的目标编码为0xf3并且通过计算观察到第1行跳转指令的下一个指令地址0x17=0xd+0xa,第7行跳转指令的下一个指令地址0xa=0xf3+0x17。

可以看出来执行PC相对的寻址时程序float表示什么计数器的值是跳转指令后面的那条指令的地址,而不是跳转指令本身的地址(缘由:当时嘚处理器以更新计数器作为执行一条指令第一步)

并且如果对目标文件链接后进行程序float表示什么反汇编,依然可以看到1和7行跳转目标的编碼始终没有变优点显而易见,使用与PC相关的跳转目标编码指令编码很简洁,目标代码无需做任何改变直接移到存储器不同位置如图:

接下来几个小节,我们将使用各种类型的跳转指令来实现C语言的控制结构从最基础的分支语句,再到循环和switch语句

C语言的if-else语句的通用形式模板:

对于这种通用形式,汇编实现一般采用如下插入条件(else-statement)和无条件分支(if-statement)的形式保证执行正确的代码块。用C语法来描述控制流:

因洏对于C语言的if-else语句需要创建一个goto版本的紧密遵循汇编代码控制流的等价语句(为了方便构造汇编代码)。基本思路是:首先比较了两个操作數(第3行),设置条件码如果比较的结果表明x大于等于y,则它会跳转到计算x-y的代码块否则继续执行计算y-x的代码。如下图所示一个计算两数之差的绝对值函数的C代码和汇编代码

C语言中提供了3种循环结构,如do-while、while、for在汇编代码中,绝大多数都可以用条件测试和跳转组合结合起来實现循环的效果并且大多数汇编器都是根据一个循环的do-while形式来产生循环代码(尽管实际中用的很少),其他的循环都是借助do-while形式再编译成机器玳码。下面就从do-while循环开始研究

do-while语句的通用形式模板:

对于这种通用形式用C语法的goto语句来描述控制流:

也就是说每次循环,程序float表示什么嘟会先执行body-statement,然后再对test-expr表达式求值如果测试为真,回去继续执行循环可看到body-statement至少执行一次。

如下图所示用do-while循环来计算函数参数的阶乘:

思路比较明确寄存器%edx保存参数n,%eax保存result。程序float表示什么开始执行循环(.L2),先执行循环的主体然后再测试n > 1,如果为真,条件跳转重复执行循环(第7行嘚跳转指令是实现循环的关键)否则退出循环返回结果。我们可以看到寄存器%eax通常用来返回函数的值所以常常选它存放要返回的程序float表礻什么值。

通过描述fact_do的过程我们学习到一个逆向工程循环的基本策略:如何由汇编代码找到和原始代码的对应关系,核心是找到程序float表礻什么值和寄存器之间的映射关系看看在循环前如何初始化寄存器,在循环中如何更新和测试寄存器以及在循环后又如何使用寄存器紦它组合起来就能打开C语言隐秘bug后的大门了。(注:GCC常会做一些寄存器优化的变换更不易观察出来)

while语句的通用形式模板:

对于这种通用形式,用C语法的goto语句来描述控制流:

和do-while不同的是它首先要对test-expr求值第一次执行body时,循环就有可能终止因而GCC常用的策略是使用条件分支,并苴在需要时优化最开始的测试省略循环的第一次执行,从而转换成do-while循环

如下图所示用while循环来实现阶乘函数:

比较fact_while和fact_do的代码,它们几乎昰相同的唯一的不同点就是初始的测试和循环的跳转(第3/4行)

for循环语句的通用形式模板:

对于这种通用形式,用C语法的goto语句来描述控制流:

咜的基本步骤是首先对初始表达式init-expr求值然后进入循环;在循环中先对测试条件test-expr求值,为假则退出否则执行循环体body-statement,最后再更新表达式。

洳下所示为for循环写的阶乘函数:

它对应的汇编代码如下所示

switch语句是一个具有多种可能结果的分支语句,它不仅提高了C代码的可读性而苴通过使用跳转表(jump table)数据结构使得实现更为高效。

所谓跳转表实际上是一个指针数组每个元素都是一个指向代码段的指针。通过表项的索引值i来执行一个跳转表内的数组引用从而确定跳转指令的目标,并且如果有重复情况的引用即简单的使用相同的代码标号

跟使用一组佷长的if-else语句相比,优点是很明显的使用跳转表执行switch语句的时间与开关情况的数量无关。甚至switch语句出现上百种情况也只用一次跳转表去訪问处理。

如下图所示为一个switch语句的示例分别是:switch语句、翻译到扩展的C语言(使用跳转表)。汇编代码略

根据汇编代码中index的值,有五个不哃的跳转位置:loc_A(.L3)、loc_B(.L4)、loc_C、loc_D、loc_def观察到jmp指令的操作数有前缀*,表明这是一个间接跳转,操作数指定一个存储器位(数组引用某索引元素)通过分析switch語句确定指针数组的每个表项具体对应哪个代码块(不同的跳转位置)

2.4 C语言函数调用的栈帧结构

IA32程序float表示什么用程序float表示什么栈来支持过程调鼡。它包括将数据(参数和返回值)和控制从代码的一部分传到另一部分另外还包括进入时为过程的局部变量分配空间,并在退出时释放空間一般地,机器只提供转移控制到过程和从过程中转移出控制的简单指令数据传递、局部变量的分配和释放必然通过程序float表示什么栈實现

机器用栈来传递过程参数、存储返回信息、保存寄存器用于以后恢复和本地存储。我们称为单个过程分配的那部分栈为栈帧按前面嘚介绍,栈指针%esp可以移动帧指针%esp不变化,因而大多数信息的访问都是相对于帧指针的如下图所示:

  • 支持过程调用和返回的指令

call指令:目标是指明被调用过程起始的指令地址。同跳转一样调用可是直接的(一个标号),也可是间接的(*后加一个操作数指示符)其效果是:将返回地址入栈,并跳转到被调用过程(P调用Q,P调用者Q被调用者)的第一条指令处。 所谓返回地址是指在程序float表示什么中紧跟call后面的那条指令嘚地址这样当被调用过程返回时,执行会从此处继续

leave指令:为返回准备栈,它等价于如下代码:

ret指令:从过程调用中返回指的是从棧中弹出地址,并跳转到这个位置

如图所示为sum和main函数的反汇编代码节选

可以看到:main函数中,call指令调用函数sum,它的效果是将返回地址0x压入栈Φ并跳到sum函数的第一条指令中。sum继续执行直到遇到ret指令它的效果是从栈中弹出值0x,然后跳到这个地址(调用sum的call函数之后),继续main函数的执行

調用者调用被调用者时要求被调用者不能覆盖某个调用者稍后会使用的寄存器值。根据惯例寄存器%eax、%edx、%ecx称为调用者保存寄存器。P调用Q時Q可以覆盖这些寄存器而不会破坏任何P需要的数据(因为会恢复)。另一方面寄存器%ebx、%esi等被划分为被调用者保存寄存器。要求Q必须在覆盖這些寄存器值之前先把它们保存到栈中,并在返回前恢复它们

2.4.2 普通函数、递归函数的调用过程

A. 普通函数的调用实例

考虑如下图定义的普通C函数调用,函数caller包括一个对函数swap_add的调用并且给出了调用swap_add函数前和正在运行时的栈帧结构。注:访问的栈位置有些是相对于栈指针%esp的有些是相对于基地址指针%ebp的,偏移量都是由相对于这两个指针的栈表示!

如图所示caller的栈帧包括存储局部变量arg1和arg2(相对于帧指针位置-4和-8,呮能存在栈中所以必须要为它们生成地址)。

第2、3、4行表示保存旧的%ebp,并把%ebp当做%esp的开始位置.然后栈指针减去24字节从而在栈上分配24个字節(栈向低地址方向生长)。第5、6行初始化两个局部变量(arg1,arg2)

第7-10行开始计算&arg2和&arg1的值并存储到栈中leal指令加载有效地址(将有效地址写入目的操作数中),形成swap_add的参数并且将这些参数存储到相对于栈指针偏移量为0和+4的地方

  • 调用swap_add的栈帧caller,分配了24个字节8个用于局部变量,8个用于向swap_add传递参數还有8个未使用。(调用swap_add前) GCC坚持一个访问数据的严格对齐原则函数使用的所有栈空间必须是16字节的整数倍。(栈分配存在一些从不使用嘚空间)

  • C语言中所有的参数都是传值调用。注意这与显示地产生一个指向某值的指针作为参数值发生改变并无冲突。传递给函数的是指針值的一个副本对函数内指针参数的改变并不会影响作为实参的指针变量,但它可以改变作为副本的指针指向的内容从而达到类似于引用参数的效果。

b. swap_add的汇编代码-初始化栈帧执行主体,过程返回恢复栈的状态

  • 初始化栈帧:在这之前call指令已经讲返回地址压入栈中。咜执行的操作就是保存旧的%ebp,%esp指针指向%ebp,并且保存旧的%ebx如下:

可以看到%ebx作为被调用者保存寄存器,它必须将旧值压入栈中注意到%ebp已经移动叻,作为swap_add的帧指针

  • swap_add主体代码:从caller的栈帧取出它的参数并执行swap_add操作如下:

由于初始化栈帧中帧指针%esp已经移动,参数的位置因而也从%esp的旧值+4囷0的位置变成了相对于%ebp的新值+12和+8的位置 寄存器%eax是作为返回值传递的

  • 过程返回恢复栈的状态:恢复%ebx和%ebp的值

恢复这几个寄存器值后,重新设萣栈指针式它指向存储的返回值ret指令将控制转移到caller,再执行caller指令的下一条指令(返回地址)

B. 递归函数的调用实例
如图为递归的阶乘函数的C代码囷汇编代码。我们主要检查当参数n来调用时机器代码如何操作。

和普通函数的过程调用类似第2-5行创建一个栈帧,其中包括%ebp的旧值、保存的被调用者寄存器%ebx的值及当递归调用自身的时候保存参数的4个字节第6-7行用寄存器%ebx来保存参数n的值,并将%eax的返回值置为1判断n < =1的结果,荿立的话会跳转到完成代码(终止条件);否则处于递归的情形下,n-1的值存在栈上并调用函数本身,此时后寄存器%eax保存着(n-1)!的值被调用鍺保存寄存器%ebx保存参数n。

从这两个示例我们可以总结出一些要点:

  • 栈规则提供了一种机制每次函数调用都有存储它自己的私有状态信息(保存的返回位置、栈指针、被调用者保存寄存器-很重要,必须先保存)
  • 如果需要还需要根据分配和释放的栈规则存储局部变量。过程调鼡中在栈上分配局部变量返回时自动释放。每个调用在各自栈中私有空间局部变量都不会相互影响。
  • 过程调用中访问信息均是相对于幀指针%ebp而言$0x4(%ebp)表示的是返回地址,往地址增大的方向$0x8(%ebp)表示的是函数第一个参数函数如果有多个参数,依次以4递增本地变量和临时变量則是往地址变小的方向存储
  • 在返回前,函数必须将栈恢复到原始条件可以恢复所有的被调用者保存寄存器和%ebp,并重置%esp使其指向返回地址。
  • 洳果在调用过程中使用了malloc函数,需要说明的是:
     一、指针变量是分配在栈上的局部变量调用结束,该变量自动释放但由malloc分配在堆嘚内存-该指针指向的堆内存却并未释放,如果不作处理就会造成内存泄露
     二、为了防止内存泄露,有两种处理情形:作为返回值返回那段堆内存的指针,从而不会丢失对那段内存的控制;在栈调用结束前使用free操作手动释放那段内存;
     三、指针变量的内存随调用結束自动释放指针指向的那段内存必须使用free或delete操作释放。因而明确一点的是free之后再解引用那个指针是非法的因为访问已释放的内存地址是无效的,一般建议释放操作后主动置指针为NULL指针就不会造成误解

2.5 C语言中的结构:数组、指针、结构、联合

对于数据类型T和整型常数N,聲明如下: T a[N]。它具有两个效果:
在存储器分配L*N个字节的连续区域L是数据类型T的大小;xa表示起始位置,则访问数组元素a[i]的位置在xa+ L*i的地方

数组え素的访问一般借助存储器引用指令。如计算整型的E[i]: E的地址存放在寄存器%edx中而i存放在寄存器%ecx中,访问指令如下:

指针实际上是地址嘚一种表示方式,它指向某一个类型的对象指针类型不是机器代码中的一部分,而是C语言提供的一种抽象

产生指针可用&运算符,它适匼于变量、结构、联合和数组的元素我们常常用leal指令来生成存储器引用的地址。*用于指针的间接引用它表示一个值,类型与指针的类型相关它是通过存储器引用来实现的,要么写入数据要么读取数据。

如下图为与整型数组E有关的表达式它的起始地址和整数索引i分別存在寄存器%edx、%ecx中

  • leal指令用来创建地址,movl用来引用存储器(除了第一种和最后一种情况前者表示复制地址,后者表示复制索引)
  • 数组与指针是囿紧密联系的一个数组的名字既可以直接数组引用又可以像一个指针变量引用(但不能修改)。如a[3]与*(a+3)有一样的效果它均需要用对象类型的夶小对偏移量进行伸缩。我们写表达式p+i,指针p的指针为p,则得到的地址计算为p+L*i,L是与p相关联的数据类型大小!
  • 指针能够从某种类型强制转换到另┅种类型只改变它的类型,而不改变它的值所起的效果是改变指针运算的伸缩。如p是一个char*类型的指针那么表达式(int*)p+7为p+28,(int*)(p+7)为p+7
  • 对于二维数组,对应的元素的地址的汇编代码表示可以借助移位、加法和伸缩组合来避免直接的乘法工作

2.5.2 结构和联合、数据对齐

C语言提供两种结合不同類型的对象来创建数据类型的机制:

  • 结构: 多个对象组合到一个单位中
  • 联合: 允许用几种不同的类型来引用一个对象

与此同时计算机系统对數据类型的对象地址必须是某值K(2、4、8)的倍数,这种对齐限制简化了处理器和存储器系统间的硬件设计Intel建议对齐数据以提高存储器系统的性能。在Linux中采用如下的对齐策略:

  • 数组作为同样大小的块来分配结构体作为变长的块来分配,保存着不同大小的结构元素;联合作为一個单独的块来分配这个块必须足够大,能够装下联合中最大的元素
  • 对于IA32系统,2字节数据类型(如short)的地址必须是2的倍数而较大的数据类型(如int、float、int *)的地址必须是4的倍数。
  • x86-64系统对齐要求更为严格对于任何需要K字节的标量数据类型的起始地址必须是K的倍数。如long、double和指针必须在8 芓节边界上对齐long double使用16字节对齐
  • 结构体的对齐除了要满足每个字段的对齐要求,还需要考虑整体的结构满足怎样的对齐要求例如32位的结構体元素里最大只有2个字节,它就无需满足4字节对齐如果含有一个4字节的元素,则整体必须满足4字节对齐即要求所有元素都满足自身嘚对齐要求(一定要了解是32还是64位系统,因为它们对齐方式有很大的不同)

下面就来具体介绍结构体和联合体:

在结构体中编译器记录了每個字段的字节偏移,汇编代码通过以结构体的地址加上适当的偏移放访问结构的字段记test的起始地址为xp,,结合数据对齐的策略:

  • 在64位机器要保证每个元素的K字节数据对齐各元素的地址为xp、xp+4、xp+8、xp+16、xp+24
  • 在32位机器要保证较大数据类型的4字节数据对齐,各元素的地址为xp、xp+4、xp+8、xp+12、xp+16

以32为例记结构体类型的指针变量在寄存器%edx中,获取每个字段元素的汇编代码如下(每行代码独立):

也就是说结构体中的元素存储的都是相对于结構体首地址的偏移回到上面的实例中,分别在64位和32位系统中测试如下

我们可以看到输出指针p和数组s名字的不同形式,这是因为指针和數组还是有区别的访问结构体中的指针字段实际上是指针本身的值(同其他非指针或数组的变量是一样),访问结构体中的数组名其实就是數组的相对地址(即char s[10]中数组名s和&s是一样的)

联合类型提供了一种方式绕过了C语言类型系统,允许以多种类型来引用一个对象并且它的总大尛为它最大字段的大小。在某些情况下联合十分有用

  • 事先知道一个数据结构中的两个不同的字段是互斥的,可将这两个字段声明为联合嘚一部分而不是结构的一部分,以减小分配空间的总量

例如实现一个二叉树的数据结构,每个叶子节点都有一个double的数据而每个内部節点都有指向两个孩子节点的指针但无数据。

每个节点需要16字节浪费一半的字节;不妨定义如下,并引入枚举类型区分联合中可能的鈈同选择,共需要12个字节

  • 访问不同数据类型的位模式。如我们以一种数据类型来存储联合中的参数又以另一种数据类型来访问它(注意此时的字节顺序)

例如以两个4字节的无符号位的形式创建一个8字节的形式:

在小端法机器上,参数w0是d的低4位字节;在大端法机器中参数w0是d的高4位字节。

刚才上面有个例子有一个char s[0]-长度为4的数组默认地零数组在标准C和C++是不允许的,如果使用的话编译时会产生错误但在GNU C99中,这种鼡法是合法的它最典型的用法是置于结构体中的最后一个字段,并且在前面至少有一个其他字段因而GCC编译时不会产生任何警告或错误。我们称这个数组为柔性数组

例如我们想定义一个可变长度的结构体,这时候我们就可以用到零数组下面是使用指针和零数组构建变長数组的代码:

对应的输出结果如下(64位系统):

从上面的结果可以看出:

  • 零长度数组定义在结构体内,但并不占用结构体的空间(可用sizeof(某结构体或鍺char[0])测试可比较指针方式是否占用空间)。可理解为这是一个没有内容的占位符标识分配了实质内容后变成了一个有长度的数组
  • 它能够为結构体内的数据分配一段连续的内存,并可以一次性讲内存释放对比指针,既需要释放指针指向的内存块又需要释放结构体指针。
  • 分配连续的内存是有利于提高访问速度并减少了一定量的内存碎片,指针则不可

汇编语言编写的代码和机器代码接近,和机器密切相关现在通常我们已经不会用它来编写程序float表示什么了。但它仍然很重要特别是希望能够了解程序float表示什么是如何转换成机器代码来运行嘚时候。本篇总结汇编语言的部分知识和C程序float表示什么如何编译成机器代码

汇编语言中不区分数据类型,将存储器看作一个很大的按字節寻址的数组程序float表示什么存储器包含程序float表示什么的目标代码、操作系统需要的一些信息、运行时栈和用户分配的存储器块。程序float表礻什么存储器用虚拟地址寻址由操作系统负责管理虚拟地址空间,将虚拟地址转换为物理地址

gcc 命令使用 -S 选项可以产生汇编代码,使用-c 選项会编译代码产生目标文件 -O2 选项为优化级别。

生成的汇编文件中 sum 函数为:

可以用反汇编器来根据目标代码来生成类似于汇编代码的格式:

sum 函数有17个字节还可以用 gdb 查看它的字节表示:

x/17xb 表示“检查17个十六进制字节”。得到的结果和 objdump 的相同

在主文件中调用 sum 函数:

反编译 m 文件,可以看到函数 suma.o 目标文件中的有两个不同:一是虚拟空间地址不同二是 accum 全局变量现在已由具体的地址代替。

Intel用字表示16位数据类型32位数称为双字,64位数称为四字见前一篇中C数据类型的大小的表。GAS中每个操作都有一个字符后缀表示操作数的大小。

IA32 CPU中包含一组8个存储32位值的寄存器它们用来存储整数数据和指针。所有8个寄存器可以以16位和32位来访问前四个寄存器还可以独立访问两个低位字节。

大多数指令具有操作数指示操作要引用的源数据值和放置结果的目的位置。操作数有三种:

  • 立即数即常数值,写为 $ 加一个整数
  • 寄存器,表礻某个寄存器的内容用E_a表示寄存器a,引用R[E_a]表示它的值
  • 存储器引用,根据地址访问某个存储器位置用M[Addr]表示存储在存储器中地址Addr开始的徝的引用。

伸缩因子s必须是1、2、4、8

最常用的指令是数据传送指令。IA32限制传送指令的两个操作数不能都指向存储器位置

IA32的栈向低地址方姠增长,压栈减小栈指针的值并存内容到存储器中出栈相反。按惯例将栈倒着画,栈顶放在下面

下表中包含了四类整数操作:加载囿效地址、一元操作、二元操作和移位操作。

左移(等价于sall)

leal 外每条指令都有以 wb 结尾的对字和字节操作的版本。

加载有效地址指令 leal 並不引用存储器而是将有效地址写到目的操作数,目的操作数必须是寄存器

下表中是一些特殊的算术操作:64位的乘积和整数除法。

程序float表示什么执行通常还需要控制操作执行的顺序汇编语言提供了非顺序控制流的机制,基本操作是跳转到程序float表示什么的另一部分

CPU中囿一组单个位的条件码寄存器,描述最近的算术或逻辑操作的属性主要包括:

  • CF :进位标志,最近的操作使最高位产生了进位可用来检查无符号操作数的溢出。
  • ZF :零标志最近的操作得到的结果为0。
  • SF :符号标志最近的操作得到的结果为负数。
  • OF :溢出标志最近的操作导致一个二进制补码溢出。

leal 指令不改变条件码其他的算术和逻辑操作都会设置条件码。此外下面的操作只设置条件码:

cmpltestl 指令都有对应嘚wb 结尾的字和字节版本。

可以根据条件码的某种组合来设置整数寄存器或执行条件分支指令

下面的指令根据条件码的组合将一个字节設置为0或1,可以用 movzbl 指令对高位字节清零来得到32位结果

大于(有符号>)
大于等于(有符号>=)
小于(有符号<)
小于等于(有符号<=)
超过(无苻号>)
超过或相等(无符号>=)
低于(无符号<)
低于或相等(无符号<=)

跳转指令使执行切换到程序float表示什么中的一个新位置,跳转的目的地通常用标号指明当跳转条件满足时,指令会跳转到一条带标号的目的地

大于(有符号>)
大于等于(有符号>=)
小于(有符号<)
小于等于(有符号<=)
超过(无符号>)
超过或相等(无符号>=)
低于(无符号<)
低于或相等(无符号<=)

jmp 指令是无条件跳转。可以是直接跳转以一个标號作为跳转目标,如 .L1 ;也可以是间接跳转跳转目标从寄存器或存储器中读出,如 *(%eax)

条件跳转只能是直接跳转。

汇编代码中跳转目标以符號标号书写汇编器和链接器会产生跳转目标的适当编码。最常用的编码是PC相关的即以目标指令的地址和紧跟在跳转指令后面的那条指囹的地址的差作为编码,这样的好处是指令编码简洁且目标代码存储位置变化时不必改变。另一种编码方法是给出绝对地址

C语言控制鋶翻译成汇编代码的结构,通常都是以跳转实现不同的编译器或同一编译器的不同优化级别产生的汇编代码的结构往往都是不同的。下媔只就一些典型的处理方式做分析实际当中会有一定不同。

C语言的条件语句是用有条件跳转和无条件跳转结合起来实现的

C语言if-else语句: 彙编实现的结构:

C语言中的三种循环也是以条件测试和跳转的组合来实现,大多数会根据循环的 do-while 形式来产生循环代码

C语言do-while语句: 汇编实現的结构:
C语言while语句: 汇编实现的结构:
C语言for语句: 汇编实现的结构:

C语言的 switch 语句的实现在分支较多和值的跨度较小时会使用跳转表。跳轉表是一个数组表项i是代码段的地址,使用跳转表使代码可以快速跳转到要执行的分支和分支数无关。

跳转表类似下面这样声明:

过程调用包括数据和控制的传递还在进入时为过程的局部变量分配空间,退出时释放空间IA32提供了转移控制的指令,数据传递和局部变量嘚分配释放则通过操纵程序float表示什么栈实现

栈用来传递过程参数、存储返回信息、保存寄存器和用于本地存储。为单个过程分配的部分棧称为栈帧寄存器 %ebp 作为帧指针,寄存器 %esp 作为栈指针栈帧以这两个指针定界。程序float表示什么执行时栈指针可以移动。

若P调用QQ的参数放在P的栈帧中。调用时P的返回地址压入栈中形成P的栈帧的末尾,返回地址是程序float表示什么从Q返回时应该继续执行的地方Q的栈帧从保存嘚帧指针开始,后面是其他寄存器的值和不能放在寄存器中的局部变量栈向低地址方向增长。

下面是过程调用和返回的指令:

调用可以昰直接的和间接的 call 指令将返回地址入栈,并跳转到被调用过程的起始处返回地址是call 后面的指令的地址。 leave 指令使栈指针指向call 存储的返回哋址 ret 指令从栈中弹出地址,并跳转到那里寄存器%eax 可以用来返回值。

为调用者保存寄存器由P保存,Q可以覆盖这些寄存器而不破坏P的数據寄存器%ebx%esi%edi 为被调用者保存寄存器,Q需要在覆盖它们之前将值保存到栈中并在返回前恢复它们。

设有数据类型 T LT 的字节大小。

C中嘚数组声明 T a[N] 在存储器中分配了 L*N 字节的连续区域设 x 为起始位置,声明还引入了标识符 a 作为指向数组开头的指针,指针的值为 x 数组元素 i 嘚存放地址为 x +

C中指针运算的值会根据指针引用的数据类型的大小进行调整。若 p 指针指向类型 T 的数据 p 的值为

对于循环中数组的引用,编译器优化通常会用指针运算代替循环变量来遍历数组用最后的数组元素的地址和指针的比较作为测试条件。

对于固定大小的数组编译器會进行多种优化,如使用指针变量来访问用移位和加法代替乘法指令。动态分配的数组在编译时不能确定大小需要用 calloc 等函数在堆中创建,必须使用乘法指令

结构的实现类似于数组,组成部分存储在连续区域指向结构的指针即结构的第一个字节的地址。编译器保存关於每个结构类型的信息指示每个域的字节偏移。

联合的语法和结构一样但它的不同的域引用相同的存储器块。

现在已经很少使用汇编玳码写程序float表示什么了但是在一些如访问寄存器或获取条件码等的场合,仍然需要汇编代码

可以编写独立的汇编代码文件,然后编译咜并和C代码链接起来GCC也支持将汇编和C代码混合起来,即内嵌汇编

内嵌汇编像过程调用一样写代码:

code-string 为字符串形式的汇编代码序列,编譯器将它插入到生成的汇编代码中错误检查会由汇编器来执行。

asm 有一个扩展版本它可以指定汇编代码序列的操作数和要被覆盖的寄存器。

中出现的顺序编号输入输出列表由, 分隔的操作数对组成,每个操作数对由空格分隔的操作数类型的字符串和括号包含的操作数组成 = 表示赋值, r 表示整数寄存器操作数是C表达式。寄存器要在前面加 %


      

通常计算机系统对基本数据类型的可允许的地址做了一些限制,要求地址必须是某个值k(常为2、4、8)的倍数对齐简化了处理器和存储器系统之间接口的硬件设计。

Linux要求2字节数据类型的地址必须是2的倍数更大的数据类型的地址必须是4的倍数。Microsoft Windows要求任何k字节数据类型的地址必须是k的倍数

编译器在汇编代码中指明全局数据所需的对齐,如跳转表中的 .align 4 它使该指令后面的数据从4的倍数的地址开始。

malloc 等分配存储器的库函数必须设计为返回的指针能满足最糟情况的对齐限制

对於结构,它的起始地址和域都有一些对齐要求如:

C对数组引用不做边界检查,同时局部变量和状态信息(寄存器值和返回指针等)都存放在栈中这使得越界的数组写操作会破坏存储在栈中的状态信息,程序float表示什么使用被破坏的状态时就会出现严重的错误

常见的状态破坏称为缓冲区溢出,就是实际保存内容的大小超过了缓冲区大小导致写越界。缓冲区溢出能被用来让程序float表示什么执行非本意的函数这是最常见的通过计算机网络攻击系统安全的方法。

GDB支持对机器级程序float表示什么的运行时评估和分析一般先运行 objdump 来获得程序float表示什么嘚反汇编版本,以帮助确定断点等断点可以设置在函数入口后面,或某个地址使用gdb 执行程序float表示什么,遇到一个断点时程序float表示什麼会停下来,将控制返回给用户在断点处,可以查看各个寄存器和存储器还可以单步跟踪程序float表示什么,一次执行几条命令或前进箌下一个断点。

下面是一些常用的命令:

run 运行程序float表示什么(设置命令行参数) nexti 执行一条指令(可以通过子例程调用) finish 运行直到当前函数返回 disas 反汇编当前函数

我要回帖

更多关于 程序float表示什么 的文章

 

随机推荐