CSCW2-Newly To Assembly

逐段分析反汇编代码:

接下来,我们对以上代码逐段分析:

int main(int argc, char *argv[]) {
  400507:    55                       push   %rbp   //建立main函数的栈帧
  400508:    48 89 e5                 mov    %rsp,%rbp   //建立main函数的栈帧
  40050b:    48 83 ec 20              sub    $0x20,%rsp   //为main函数分配栈帧空间,此处为32个字节
  40050f:    89 7d ec                 mov    %edi,-0x14(%rbp)   //将系统传给main函数的第一个参数复制到自己的栈帧(可以查看值吗?)
  400512:    48 89 75 e0              mov    %rsi,-0x20(%rbp)   //将系统传给main函数的第二个参数复制到自己的栈帧,,并存放在栈帧的最顶部(可以查看值吗?)

此时通过:

(gdb) x/1xg $rbp-0x20
0x7fffffffe550:    0x00007fffffffe658
(gdb) x/1xw $rbp-0x14
0x7fffffffe55c:    0x00000001

我们发现传递给main函数的第一个参数的值是1,而第二个参数的值貌似是一个内存地址(值的引用/指针)。并且可以看出函数的实参列表(Arglist)在栈帧中的地址是从高向低分配的。 同时我们认为main函数的栈帧是从此时的$rbp+0x10开始的,也就是在main函数栈帧的底部里还保存了rbp和rip。 (很奇怪的是两个参数之间存的值竟然是_start函数的的4字节地址,这里是考虑了8个字节的对齐吗?)

接着看:

    int a,b,result;

    a = 1;
  400516:    c7 45 f4 01 00 00 00     movl   $0x1,-0xc(%rbp)   //此处为给main函数的局部变量a赋值过程,将该值复制到main函数的栈帧中;使用movl表明这个变量是4个字节的
    b = 2;
  40051d:    c7 45 f8 02 00 00 00     movl   $0x2,-0x8(%rbp)   //此处为给main函数的局部变量b赋值过程,将该值复制到main函数的栈帧中

我们发现,打在C程序代码第12行的断点,在汇编代码实际断点在这里,也可以看出函数局部变量(Locals)的地址是从低向高分配的。而且一个有趣的现象是Arglist和Locals的起始地址都是-0x10(%rbp)。

接着看:

    result = add(a,b);
  400524:    8b 55 f8                 mov    -0x8(%rbp),%edx   //这里把传递给add函数的第二个参数的值复制到寄存器edx
  400527:    8b 45 f4                 mov    -0xc(%rbp),%eax   //这里把传递给add函数的第一个参数的值复制到寄存器eax
  40052a:    89 d6                    mov    %edx,%esi   //接着把传递给add函数的第二个参数值从edx寄存器复制到esi寄存器
  40052c:    89 c7                    mov    %eax,%edi   //接着把传递给add函数的第一个参数值从eax寄存器复制到edi寄存器
  40052e:    e8 ba ff ff ff           callq  4004ed <add>   //这里call命令让整个程序的执行流跳转到地址(地址长度是一个字节)0x4004ed(也就是add函数的地址)处,
  400533:    89 45 fc                 mov    %eax,-0x4(%rbp)   //(暂停main函数的分析,进而分析add函数;注意这条指令的地址在该函数的上一条指令被执行前,存入eip寄存器了)

这里实际的情况印证了之前讲到的,rdi,rsi,rdx,rcx,r8d,r9d这6个寄存器会依次暂存主调函数传给被调函数的参数。而之所以中间还要转存到edx和eax是因为,cpu从寄存器中读取数据的速度远远大于从内存中读取数据的速度;为了提高性能,一旦内存中一个参数被使用,那么先会被暂存到一个空余的寄存器中,以后再使用时,就不用从内存中读取了。

我们也可以看出在存取传递给函数的参数时,是从右向左读取的。
eip的工作原理是,cpu读取当前eip指向的指令,存入指令缓冲器(指令队列)中,然后eip根据被读取指令的长度,增加相应的字节数,指向下一条指令,然后cpu执行指令队列中刚刚读取的指令。

call这个跳转指令在执行时,实际分为两步,一个是先pop该指令执行时eip的值(即主调函数的调用发生时的下一条指令地址)到当前函数的栈帧中(当前栈帧增长,esp的值会减小8个字节),然后程序的执行流跳转到相应的地址处,即eip的值等于相应的地址(此处即为add函数的地址处0x00000000004004ed)。

接着看add函数的内部:

00000000004004ed <add>:
int add(int a, int b) {
  4004ed:    55                       push   %rbp   //首先把主调函数的栈基址入栈(栈增长,esp的值减小8个字节)
  4004ee:    48 89 e5                 mov    %rsp,%rbp   // 让当前基址指针指向主调函数的栈顶
  4004f1:    89 7d ec                 mov    %edi,-0x14(%rbp)   //将主调函数传给add函数的第一个实参复制到add函数的栈帧
  4004f4:    89 75 e8                 mov    %esi,-0x18(%rbp)   //将主调函数传给add函数的第二个实参复制到add函数的栈帧

系统并没有像在main函数里的那样,显式地给add函数分配栈帧空间,原因是add函数内并不调用其他函数,因此没有必要让esp的值再发生变化。所以实际上add函数的栈帧的顶部和其主调函数的栈顶重合。 同时我们认为add函数的栈帧开始于此时的$rbp+0x10, (这里为什么要保留16个字节的空间没有使用呢?)

接着:

    int result;

    result = a + b;
  4004f7:    8b 45 e8                 mov    -0x18(%rbp),%eax   //将加法运算的第二个操作数的值从栈中复制到eax寄存器
  4004fa:    8b 55 ec                 mov    -0x14(%rbp),%edx   //将加法运算的第一个操作数的值从栈中复制到edx寄存器
  4004fd:    01 d0                    add    %edx,%eax   //执行加法运算,并将值保存在eax寄存器中
  4004ff:    89 45 fc                 mov    %eax,-0x4(%rbp)   //将eax寄存器中的值(得到的和)复制到add函数的栈帧中(这个地址就是add函数的局部变量result的地址)

这里验证了之前讲到的,在使用内存中定义的一个值时,会先把它复制到一个寄存器中暂存起来。

接下来:

    return result;
  400502:    8b 45 fc                 mov    -0x4(%rbp),%eax   //将返回值result复制到寄存器eax中
}
  400505:    5d                       pop    %rbp   //将add函数栈帧的栈顶值(上一个函数的栈基址)弹出到rbp寄存器中
  400506:    c3                       retq  //ret指令从栈中弹出地址,并跳转到这个地址,这里相当于把值弹出到eip寄存器中。

这里验证了前面讲的eax寄存器经常存储被调函数的返回值。执行到这里后,由于之前栈中压入的eip,跳转到 # callq 400ed #指令的下一条指令。

然后:

  400533:    89 45 fc                 mov    %eax,-0x4(%rbp)   //将被调函数的返回值复制到main函数栈帧中(局部变量result)

    return 0;
  400536:    b8 00 00 00 00           mov    $0x0,%eax   //main函数向它的主调函数返回值0
}
  40053b:    c9                       leaveq   //leave指令可以使栈做好返回准备,
  40053c:    c3                       retq  //同之前介绍的一样
  40053d:    0f 1f 00                 nopl   (%rax)

leave指令相当于以下两条指令:

mov %rbp,%rsp
pop %rbp