计算机组成原理
要查看目标代码(.o文件),最常用的使用反汇编器。在Linux中是命令objdump -d file.o可以调用程序OBJDUMP充当这个角色。但是它产生的类似汇编代码格式的文本和由gcc生产的汇编代码的字节序列有细微差别,比如前者省略了表示大小的后缀。
数据格式
Intel通常用”字“来表示16位的数据类型,因此,32位数为”双字"。注意:虽然汇编代码中,都是使用后缀“l"表示4字节的整数和8字节的双精度浮点数,但是不存在歧义,因为浮点数使用的是完全不同的指令的寄存器。
在IA32的CPU中有8个32位的寄存器。大多数指令不以固定的寄存器作为源/目标寄存器。不过,%ebp(Extended Base Pointer)和%esp(Extended Stack Pointer,栈顶指针)保存着指向程序栈(在IA32中,程序栈被放在存储器的某一个区域中并向下增长)的重要位置的指针,按照惯例,%eax(Extended Accumulator Register)通常用来存储函数返回值和累加器。此外,我们还可以单独地读取某些寄存器的低位字节。比如字操作指令可以只读写寄存器的低16位,其余的字节不会改变。
简单介绍MOV类指令
movl | 传送双字 |
movwl | 将做了符号扩展的字传送到双字 |
movwl | 将做了零扩展的字传送到双字 |
pushl S |
R[%esp] <--- R[%esp] + 4 M[R[%esp] <--- S |
popl D |
D <--- M[R[%esp]]; R[%esp] <--- R[%esp] + 4 |
算术和逻辑操作
leal(load effective address)最后的”l"比较迷惑人,看起来像是处理双字大小的,然而实际上leal指令没有大小变种,不像add类,mov类指令。leal实际上是movl的变种,指令形式是从存储器读数据到寄存器,如,但是并没有真正引用存储器,它只是计算了一下地址,如"leal 7(%edx,%edx,4), %eax",若%edx存储的是x,则就将寄存器%eax设置为x+4x+7=5x+7。形式化可以写作:leal S, D意为D<---&S,加载有效地址。
SAL k, D | 左移 |
SHL k, D | 左移(等价于SAL) |
SAR k, D | 算术右移 |
SHR k, D | 逻辑右移 |
控制
条件码
除了整数寄存器,CPU还维护了一组单个位的条件码寄存器,他们描述了最近的算术或逻辑运算操作的属性。我们可以通过检测这些寄存器的值来执行条件分支指令。
CF:进位标志。最近的操作使最高位产生了进位。可以用来检查无符号操作数的溢出。
ZF:零标志。最近的操作得出的结果为0。
SF:符号标志。最近的操作得到的结果为负数。
OF:溢出标志。最近的操作导致一个补码的溢出——正溢出或负溢出。
假设执行了一个加法运算t=a+b(t,a,b都是整型),可以根据如下表达式来设置条件码:
CF | (unsigned) t < (unsigned) a | 无符号溢出 |
ZF | (t == 0) | 零 |
SF | (t < 0) | 负数 |
OF | (a < 0 == b < 0) && (t < 0 != a < 0) | 有符号溢出 |
注意:leal不改变任何条件码。对于XOR,进位标志和溢出标志会设置为0。对于移位操作,进位操作会被设置为最后一个被移出的位,溢出标志设置为0。这里强调一下CMP和TEST指令。CMP和SUB指令的行为是一样的(CMP S2, S1: S1 - S2),而CMP只改变条件码,不更新目标寄存器。当两个操作数相等时,ZF会被设置为0,其他标志则可以用来判断两个数的大小。TEST和AND(按位与&)指令的行为是一样的,而TEST只改变条件码。当两个操作数是一样的,如指令testl &eax, %eax通常被用来检查%eax是正数、负数还是零。然而条件码通常不会直接读取。我们通过SET类指令针对条件码的不同组合而设置值。SET类指令的操作数只能是单字节寄存器或一个字节的存储器位置。
JMP类指令当执行于PC(programming counter)寻址时,程序计数器的值是跳转指令后面的那条指令的地址,而不是跳转指令本身的地址。比如
jb指令的跳转目标是0x8048340,其对应的目标编码为72 0xe7(72是jb指令的表示,0xe7是-25的补码表示)。因此0x8048359-25=0x8048340。
条件分支
C语言中的if-else语句的通用形式模板:
if(test-expr)
then-statement
else
else-statement
对于这种通用形式,汇编实现通常会使用下面这种形式
t = test-expr
if(!t)
goto false;
then-statement
goto done;
false:
else-statementdone:
举例说明,以下是一段C语言对应的反汇编代码
x at %ebp+8, y at %ebp+12 | |
1 movl 8(%ebp), %eax | 将地址为%ebp+8的值转移到%eax(x)中 |
2 movl 12(%ebp), %edx | 将地址为%ebp+12的值转移到%edx(y)中 |
3 cmpl $-3, %eax | 将x与-3进行比较 |
4 jge .L2 | 若x大于或等于-3,则跳转到L2 |
5 cmpl %edx, %eax | 将x与y进行比较 |
6 jle .L3 | 若x小于或等于y,则跳转到L3 |
7 imull %edx, %eax | %eax = %eax * % edx (x * y) |
8 jmp .L4 | 无条件跳转到L4 |
9 .L3: | |
10 leal (%edx, %eax), %eax | %eax = %eax + %edx (x + y) |
11 jmp .L4 | 无条件跳转到L4 |
12 .L2: | |
13 cmpl $2, %eax | 将x与2进行比较 |
14 jg .L5 | 若x大于2,则跳转到L5 |
15 xorl %edx, %eax | %eax = %eax ^ %edx (x ^ y) |
16 jmp .L4 | 无条件跳转到L4 |
17 .L5 | |
18 subl %edx, %eax | %eax = %eax - %edx (x - y) |
19 .L4 |
通过分析汇编代码,我们可以很容易完成C代码的填空。C代码的第一个if对应于汇编语言的第3行,并注意将条件取反,因此为x<-3,此外,易得汇编语言的L2对应于C代码的8、9行。因此C语言的第8行应填写x<=2,第9行填写x^y,第2行填写x-y。同理,C代码的第4行应该填写y<x,第5行填写x*y,第7行填写x+y。注意:这里的初始化表达式(C代码的第2行)向下移了(移到了汇编代码15行),这样一来,只有当确定它就是返回值的时候,才会计算它。
循环
C语言提供的多种循环结构,即do-while、while和for循环。因为汇编没有相应的指令存在,但是可以使用条件测试和跳转组合结合起来实现循环的效果。大多数的编译器根据一个循环的do-while形式来产生循环代码。其他的循环会首先被转换成do-while形式,然后再翻译成机器代码。do-while循环的通用形式如下:
do
body-statement
while(test-expr);
可以看到,body-statement至少会被执行一次。上述的do-while的通用形式可以翻译为如下的条件和goto语句
loop:
body-statement
t = test-expr;
if(t)
goto loop;
while语句的通用形式如下
while(test-expr)
body-statement
将while循环翻译成机器代码有很多方法,采用gcc的策略,使用条件分支,将其转换为do-while循环,如下
t = test-expr;
if(!t)
goto done;
loop:
body-statement
t = test-expr;
if(t)
goto loop;
done:
for循环的通用形式如下
for (init-expr; test-expr; update-expr)
body-statement
把它翻译为goto代码为
init-expr;
t = test-expr;
if(!t)
goto done;
loop:
body-statement
update-expr;
t = test-expr;
if(t)
goto loop;
done: