想要把函数调用时的过程搞清楚,这里是一些记录。
注:因为是linux系统下的调试,汇编代码使用的是AT&T格式。
首先,linux系统会将1GB的空间分配给内核,这1GB的内存占据4GB内存的高地址段,即0XC0000000~0XFFFFFFFF。而栈底就从0Xbfffffff开始,平时用GDB调试的时候观察一下esp和ebp,基本都是在0xbfff....这个位置。
先看一下最简单的情况
{
int a=4, b=5;
return 0;
}
编译后用objdump -d a.out来观察main函数的汇编代码
08048394 <main>:
8048394: 55 push %ebp
8048395: 89 e5 mov %esp,%ebp
8048397: 83 ec 10 sub $0x10,%esp
804839a: c7 45 fc 04 00 00 00 movl $0x4,-0x4(%ebp)
80483a1: c7 45 f8 05 00 00 00 movl $0x5,-0x8(%ebp)
80483a8: b8 00 00 00 00 mov $0x0,%eax
80483ad: c9 leave
80483ae: c3 ret
80483af: 90 nop
最开头的两句:
push %ebp
mov %esp, %ebp
作用是将ebp原来的值保存在栈上。由于是push操作,esp栈顶指针指向栈顶,此时mov %esp, %ebp,让ebp的值等于esp,作用就是重新开辟来一个新的栈,函数main的栈帧。
之后一句:
sub $0x10, %esp
字面意思是将esp的值减0x10,十进制的16。本来esp指向的是栈底,现在减了16个字节(栈的增长是由高地址向低地址减少),也就是栈顶和栈底之间有了16个字节的空间。一个int型变量占4个字节,16/4等于4,可以在这段范围内放4个int型的变量。ebp是栈底,至少在这个函数运行期间不会改变,就可以用ebp减去一个值这样的方法在栈上寻找变量。
c7 45 fc 04 00 00 00 movl $0x4, -0x4(%ebp)
这句对应源代码里面的a=4;
$0x4是一个立即数。
c7 45 fc 04 00 00 00是机器码,可以看到这句最后面的四位04 00 00 00就是立即数4,因为x86是小端字节序,所以04放在最前面。
c7 45 fc 就是对应的movl $0x4,-0x4(%ebp)的指令。-0x4(%ebp)这是一个地址的表达式,表示ebp-4。这条语句的作用就是将立即数4保存到ebp-4的内存中。这里,用-4($ebp)这种方式在栈上定义、寻找变量。和我们平时理解中用push和pop入栈出栈有些不一样,直接用ebp-x在栈上进行操作。
c7 45 f8 05 00 00 00 movl $0x5,-0x8(%ebp)
这句和上一句一个意思,只是指令有一点点区别,地址上用-0x8(%ebp)来表示,这是变量b=5。
b8 00 00 00 00 mov $0x0,%eax
return的是0,这里用eax存放函数的返回值。
c9 leave
c3 ret
90 nop
这三条是函数结束时的操作。
函数(以main为例)调用另一个函数时,会有一些小的动作,如下:
1、参数入栈。
2、把当前指令的下一条指令的地址入栈。
3、跳转到函数体执行。
其中2和3由指令call一起执行
一个函数被调用,也会在函数开始的时候,做一些小动作,如下:
1、push $ebp(是上一个函数栈帧的ebp)
2、mov $esp, $ebp(设置栈底)
3、sub 0x??, $esp (根据需要开辟??大小的空间)
4、push 寄存器 (如果有需要)
该函数返回时会有如下动作(基本于上面的顺序相反)
1、pop 寄存器 (如果有需要)
2、mov $ebp, $esp (将栈顶直接指到栈底,此时栈上的东西已不再受到控制)
3、pop $ebp (将上一个函数的ebp弹出,回到主调函数栈帧内)
4、ret (从栈中取得返回地址,并跳转到该位置
2和3由指令leave一起执行。
再来一段简单的函数调用的代码
int foo(int a, int b)
{
int c ;
a = 3, b = 4;
c = a + b;
return c;
}
int main(void)
{
int n, m;
n = 1, m = 2;
foo(n, m);
return 0;
}
08048394 <foo>:
8048394: 55 push %ebp
8048395: 89 e5 mov %esp,%ebp
8048397: 83 ec 10 sub $0x10,%esp
804839a: c7 45 fc 03 00 00 00 movl $0x3,-0x4(%ebp)
80483a1: c7 45 f8 04 00 00 00 movl $0x4,-0x8(%ebp)
80483a8: 8b 45 f8 mov -0x8(%ebp),%eax
80483ab: 8b 55 fc mov -0x4(%ebp),%edx
80483ae: 01 d0 add %edx,%eax
80483b0: 89 45 f4 mov %eax,-0xc(%ebp)
80483b3: 8b 45 f4 mov -0xc(%ebp),%eax
80483b6: c9 leave
80483b7: c3 ret
080483b8 <main>:
80483b8: 55 push %ebp
80483b9: 89 e5 mov %esp,%ebp
80483bb: 83 ec 18 sub $0x18,%esp
80483be: c7 45 fc 01 00 00 00 movl $0x1,-0x4(%ebp)
80483c5: c7 45 f8 02 00 00 00 movl $0x2,-0x8(%ebp)
80483cc: 8b 45 f8 mov -0x8(%ebp),%eax
80483cf: 89 44 24 04 mov %eax,0x4(%esp)
80483d3: 8b 45 fc mov -0x4(%ebp),%eax
80483d6: 89 04 24 mov %eax,(%esp)
80483d9: e8 b6 ff ff ff call 8048394 <foo>
80483de: b8 00 00 00 00 mov $0x0,%eax
80483e3: c9 leave
80483e4: c3 ret
80483e5: 90 nop
可以看到,foo函数的最后一条指令地址是80483b7,就一个字节的指令ret(c3)。紧接着的80483b8就是main函数的第一条指令来。这个顺序和源代码中函数位置有关。
先看main函数。很标准的
push $ebp
mov $esp, $ebp
sub $0x18, $esp (在栈上开辟来0x18的空间,十进制的24,可以放6个4字节的变量、地址)
下面两句mvol就是给n和m赋值。这条指令执行完毕后,ebp=0xbfffeff8(方便起见用ff8表示,esp一样),esp=0xbfffefe0,栈上内容如下:
内存地址 该地址内4字节内容
0xbfffefe0: 0x4f0c0384 0x4ef11fc4 0x080483f9 0x4f0bfff4
0xbfffeff0: 0x00000002 0x00000001 0x00000000
可以看到地址ff8(ebp)内保存的是0x00000000,ebp这个位置应该是放old ebp(主调函数ebp的值),也许是因为main函数的关系,这里都是0,先不管。
ff4(ebp-4)里面的内容是int型的1,就是源代码中变量n。ff0(ebp-8)里面的内容是int型的2,就是源代码中变量m。后面的fec~fe4这几个位置都是一些未初始化过的垃圾数据。
当这两句movl执行完毕之后,接下来的就是为调用foo函数作准备工作,即参数入栈,main函数中foo()语句之后的下一条指令return 0的地址入栈,再跳转到foo函数。
下面两条条指令
80483be: 8b 45 f8 mov -0x8(%ebp),%eax (这句将ebp-8的内容,也就是变量m的值放进eax)
80483cf: 89 44 24 04 mov %eax,0x4(%esp) (这句将eax的内容保存到esp+4,注意,这里用的是esp,这个动作就是参数入栈。因为用的是默认的调用惯例cdecl,函数的参数从右向左入栈,foo(n, m),于是先将m入栈)。
80483d3: 8b 45 fc mov -0x4(%ebp),%eax
80483d6: 89 04 24 mov %eax,(%esp)
这两句的作用是将参数n入栈。此时foo函数的参数m和n,与main函数中定义的m与n,在地址上是分开的。或者说,刚才那四条指令在内存中保存的数据,已经是foo函数自己内部的变量了,所以改变这两个变量,不会对main函数中的m和n有任何改变。
80483d9: e8 b6 ff ff ff call 8048394 <foo>
80483de: b8 00 00 00 00 mov $0x0,%eax
这里有一句call指令。call有两个作用,先将call指令后面那条指令的地址,也就是mov $0x0, %eax这条指令的地址80483de入栈。所以在foo函数的栈帧上会看到这个数字。地址入栈后,跳转,开始执行foo函数的指令。
08048394 <foo>:
8048394: 55 push %ebp
8048395: 89 e5 mov %esp,%ebp
8048397: 83 ec 10 sub $0x10,%esp
804839a: c7 45 fc 03 00 00 00 movl $0x3,-0x4(%ebp)
80483a1: c7 45 f8 04 00 00 00 movl $0x4,-0x8(%ebp)
80483a8: 8b 45 f8 mov -0x8(%ebp),%eax
80483ab: 8b 55 fc mov -0x4(%ebp),%edx
80483ae: 01 d0 add %edx,%eax
80483b0: 89 45 f4 mov %eax,-0xc(%ebp)
80483b3: 8b 45 f4 mov -0xc(%ebp),%eax
80483b6: c9 leave
80483b7: c3 ret
foo函数开始也是push,mov,sub这一个套路。
sub $0x10, %esp,在栈上开了16个字节,可以放4个变量。这时候,看下寄存器的内容,可以看到esp=fc8,ebp=fd8。我们看一下这个时候栈上的内容(为了作对比,把main函数刚才的内容一起复制过来)
再强调一下,x86下,栈由高地址向低地址变化,push入栈的操作会让esp-4,pop则会让esp+4。gdb调试的时候,显示出来的内存内容则是由低向高增长,所以看的时候要从后往前看。
foo函数栈上内容
内存地址 该地址内4字节数据
0xbfffefc8: 0x080496c4 <- foo栈顶 0x08048411 0x080483f0 0x080482e0
0xbfffefd8: 0xbfffeff8 <-foo栈底 (old ebp) 0x080483de 0x00000001<-main栈顶 0x00000002
0xbfffefe8: 0x080483f9 0x4f0bfff4 0x00000002 0x00000001
0xbfffeff8: 0x00000000<-main栈底
(old ebp就是main函数栈帧中的ebp: ff8)
main函数栈上内容
内存地址 该地址内4字节数据
0xbfffefe0: 这个是栈顶-> 0x00000001 0x00000002 0x080483f9 0x4f0bfff4
0xbfffeff0: 0x00000002 0x00000001 0x00000000 <-此处是栈底
可以看到,main的栈顶之后,foo的栈底之前还有一个值,4个字节,地址为fdc,值为0x080483de,其实就是call指令执行时压入栈中的返回地址。main函数和foo函数的栈帧在内存上是相接的,都没有空隙。由于刚进入foo函数,所以栈上的内容都是无用的垃圾。
804839a: c7 45 fc 03 00 00 00 movl $0x3,-0x4(%ebp)
80483a1: c7 45 f8 04 00 00 00 movl $0x4,-0x8(%ebp)
这两条指令对应源代码中的a = 3, b = 4。进入foo函数后,esp和ebp已经通过上面那三条指令改变过,都是在foo的栈帧范围内。ebp-4的位置存放3, ebp-8的位置存放4。再看下栈上的变化
0xbfffefc8: 0x080496c4<-foo栈顶 0x08048411 0x00000004 0x00000003
0xbfffefd8: 0xbfffeff8<-foo栈底 0x080483de 0x00000001 0x00000002
0xbfffefe8: 0x080483f9 0x4f0bfff4 0x00000002 0x00000001
0xbfffeff8: 0x00000000
我们看到fd4(ebp-4)和fd0(ebp-8)的位置都已经初始化了。
80483ae: 01 d0 add %edx,%eax
80483b0: 89 45 f4 mov %eax,-0xc(%ebp)
这两句对应c=a+b。可以看到变量c的位置在ebp-0xc
80483b3: 8b 45 f4 mov -0xc(%ebp),%eax
这里在为return作准备,将需要return的值放入eax。
之后收尾。
栈上基本的东西弄明白来,但也有几个地方的值我也没搞清楚,只能先存疑了。