接下来,我们对以上代码逐段分析:
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