分析高级语言编译后生成的汇编语言。
运行如下命令得到C语言的汇编代码:
unix> gcc -O1 -S code.c
gcc -c选项编译源文件生产目标文件code.o:
unix> gcc -O1 -c code.c
可以使用反汇编器(disassembler)来查看目标文件内容:readelf/objdump
unix> objdump -d code.o
gcc -o选项可连接不同的目标文件,生产可执行文件:
gcc -O1 -o prog code.o main.c
可执行文件prog比单纯的目标文件大了许多,因为它不仅包含了源文件的代码,还包含了与操作系统交互的程序(用于启动和终止程序等)。objdump也可用于分析可执行文件:
unix> objdump -d prog
大多数GCC生成的汇编代码都有一个字符后缀,表明操作数大小。如mov指令有3个变种:movb(字节)、movw(字)、movl(双子)。
CPU(IA32)包含8个32bits的寄存器,寄存器以%开头,可表示8位(%al, %ah)、16位(%ax)、32位(%eax)数据
多少汇编指令都有操作数来指定操作中使用的数据,以及结果存放的位置。IA32支持多种操作数格式
Imm一般表示立即数或者偏移量,Eb表示基址寄存器,Ei表示变址寄存器,s为比例因子。
立即数和寄存器很好理解,第三个绝对寻址就是用常数(或常数表达式)直接给出操作数在内存中的地址,第四个间接寻址就是将内存地址放在寄存器里,第五个是基址(寄存器的值,基址寄存器)加上偏移量(Imm的值)形成的地址,第六个就基址(基址寄存器)加上变址(变址寄存器)形成的地址,第七个是前一个的基础上加一个偏移,剩下的比例变址寻址就是前面的值相加再加上变址寄存器与比例因子的乘积(Ei * s)。
复杂的寻址方式比较适合于数组和结构元素。
mov和push、pop都可以传送数据
mov系列指令有两个操作数,IA32限制他们不能都为内存地址,但是都可以使寄存器。
MOVS和MOVZ都是将一个较小的源数据复制到一个较大的数据位置,高位(就是目的数据多出的bit位)用符号位(MOVS)或者零(MOVZ)填充。
3种mov指令的例子如下:
下图列出了一些整数和逻辑操作,除leal外,其他指令都有根据操作数大小而不同的变种,如ADD有3种:addb(字节)、addw(字)、addl(双字)。这些指令分为四类:加载有效地址、一元操作、二元操作和移位
指令 leal 将有效地址写入目的操作数(只能为寄存器),如果寄存器%edx的值为x,那么指令 leal 7(%edx,%edx,4),%eax 将寄存器%eax的值设置为 5x + 7,而movl指令会将内存中 5x + 7 位置上的数据送入目的操作数。是不是有点像指针?
二元指令中操作数不能都是内存位置。
下图是一些特殊的算术操作
条件控制、循环控制、分支控制。
CPU有一个条件码寄存器,记录一些标志位,常用标志位:
CF:进位标志,carry
ZF:零标志,zero
SF:符号标志,sign
OF:溢出标志,overflow
运算指令(除leal外)都会影响条件寄存器。
CMP与TEST指令进行实际运算,只改变条件码。CMP与SUB一样对两个操作数执行减法,根据结果设置相应标志位;TEST与ADD一样对两个操作数执行加法,根据结果设置相应标志位。他们也分别有3种变种:b、w、l
三种常用方法:根据条件码设置一个数据为0或者1;根据条件码跳转到其他部分;有条件地传送数据。
使用SET指令,可以根据条件码设置一个字节为0或1
JMP 系列指令跳转到指定位置执行代码,或者根据条件码决定要不要跳转到新的代码位置。
图a为计算两数之差的C代码,图b为对应的使用goto语句的C代码,图c为GCC生成的汇编代码
C语言中的if-else语句形式如下:
if (test-expr)
then-statement
else
else-statement
对应的汇编形式如下:
t = test-expr;
if (!t)
goto false;
then-statement;
goto done;
false:
else-statement;
done:
C语言中的循环:do-while、while、for,汇编指令动态条件测试和跳转指令来实现。
5.5.1 do-while
do
body-statement
while (test-expr)
翻译成如下goto形式
loop:
body-statement
t = test-expr;
if (t)
goto loop;
while语句通用形式:
while (test-expr)
body-statement
先转换成do-while形式:
if (!test-expr)
goto done;
do
body-statement
while (test-expr)
done:
再转换成do-while对应的goto形式:
t = test-expr;
if (!it)
goto done;
loop:
body-statement
t = test-expr;
if (t)
goto loop;
done:
实例如下:
for循环通用形式如下:
for (init-expr; test-expr; update-expr)
body-statement
转换成while循环的形式:
init-expr;
while (test-expr) {
body-statement
update-expr;
}
再转换成do-while形式:
init-expr;
if (!test-expr)
goto done;
do {
body-statement
update-expr;
} while (test-expr);
done:
最后转换成对应的goto形式:
init-expr;
t = test-expr;
if (!t)
goto done;
loop:
body-statement
update-expr;
t = test-expr;
if (t)
goto loop;
done:
条件传送指令cmov根据条件码选择是否将一个值复制到一个寄存器
编译器会使用跳转表(jump table)来实现switch语句。跳转表是一个数组,表项i是一个代码段地址,当switch的值为i时,程序就指向表项i地址上的代码。比使用分支语句性能好很多。
大多数机器只提供转移控制到过程和从过程中转移出控制的指令,数据传递、局部变量分配和释放通过程序栈完成。
栈用于传递过程(函数)参数、存储返回信息、保存寄存器以恢复调用前状态,以及本地存储。为某一个过程(函数)分配的那部分栈称为栈帧(stack frame)。下图为栈帧的通用结构。栈帧的顶端用两个寄存器确定,%ebp为帧指针、%esp为栈指针。
假如过程P(调用者)调用过程Q(被调用者),那么Q的参数存在P的栈帧中,P的返回地址也被压入P的栈中(P栈帧的尾部)。返回地址就是从Q返回到P后继续执行的地方(也就是调用P语句之后的那条语句)。Q的栈帧从保存的帧指针的值(%ebp)开始。
过程Q还用栈保存其他不能放在寄存器中的局部变量
第一个参数放在相对于%ebp偏移量为8的位置处,剩下的参数(假设参数数据大小不超过4)存储在后续的4字节块中,那么第i个参数在相对于%ebp偏移量为4 + 4i的位置。
过程调用和返回的指令
call指令将返回地址压入栈,跳转到被调用过程起始处,返回地址就是调用者中call指令下一条指令。ret指令从栈中弹出返回地址,从此处继续执行。
调用过程中寄存器的保存:
%eax、%edx和%ecx由调用者保存和恢复
%ebx、%esi、%edi由被调用者保存和恢复
%ebp和%esp也由被调用者保存和恢复(可以使用leave指令)
数组声明:T A[N];
存储器为它分配一个 L*N 字节的连续区域,其中 L 是类型 T 的大小。x 为起始地址(第一个元素地址),那么第 i 个元素地址为 x + L*i。
把起始地址 x 存在 %edx 中,类型大小 T 存在 %ecx 中,我们就可以使用比例变址寻址来访问数组元素:
movl (%edx, %ecx, 4), %eax
整型数组 E 的起始地址和索引 i 分别存放在 %edx 和%ecx中,下图为 E 相关的表达式:
一个二维数组:int A[5][3]; 等价于下面的声明:
typedef int row3_t[3];
row3_t A[5];
对于一个二维数组声明:T D[R][C];假设数组起始地址为 x ,数据长度为 L ,元素 A[i][j] 的存储器地址为:&A[i][j] = x + L(C*i + j);
C语言中使用struct和union来创建数据类型。
struct将不同类型的数据结合到一个对象中,存储在连续区域,以偏移来定位结构中的字段(field)。
下面是一个struct声明
struct rec {
int i;
int j;
int a[3];
int *p;
};
它包含2个4字节的int类型,1个3*4字节的int数组,以及一个4字节的地址,一共24个字节:
通过结构开始地址和字段偏移量来访问结构中的字段,假如开始地址存在 %edx 中,那么 (%edx) 就是 i,4(%edx)就是j
联合允许一个对象里存储不同类型的数据,用不同的字段来引用相同的内存块。
union U3 {
char c;
int i[2];
double v;
};
对于联合类型U3,里面有3个(种)字段,它们长度分别为:1,8,4,,联合的长度就是包含的字段的最长值,所以U3的长度为8。
某些计算机系统要求某种类型对象的地址必须为某个值(2,4或8)的倍数,这就是数据对齐。数据对齐简化了硬件间的接口设计,并且可以提高存储器性能。
Linux的对齐策略为,2字节数据类型(如short)的地址为2的整数倍,较大的数据类型(如int、int*、float和double)地址必须为4的整数倍。
指针映射到机器代码的关键原则: