计算机组成原理

要查看目标代码(.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-statement

done:

举例说明,以下是一段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:

 

posted @ 2018-01-18 15:46  Jelly08  阅读(414)  评论(0)    收藏  举报