c语言函数与变量的内存布局
今天突然想知道C中函数是怎么在内存中分布的,虽然肯定是听过或看过结论的,但是还是想自己亲手试试,一探究竟!
目录:
- 测试的源代码
- 测试的汇编代码
- gdb中的汇编代码
- 调试程序
- 总结
测试的源代码
测试的文件比较简单,写了一个sum函数,计算3个参数的和并返回。
test.c:
#include <stdio.h>
int sum(int x,int y,int z){
int sum = x + y + z;
return sum;
}
int main(int argc,char ** argv){
int x,y;
x = sum(1,2,3);
y = sum(4,5,6);
return 0;
}
测试的汇编代码
使用gcc -S test.c,会自动在当前目录下生成test.s文件,其内容如下:
注:这里只是贴出文件内容,但是其对后面的分析并没有什么作用。分析使用的是整理后的汇编代码。
.file "test.c"
.text
.globl sum
.type sum, @function
sum:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl %edi, -20(%rbp)
movl %esi, -24(%rbp)
movl %edx, -28(%rbp)
movl -24(%rbp), %eax
movl -20(%rbp), %edx
addl %eax, %edx
movl -28(%rbp), %eax
addl %edx, %eax
movl %eax, -4(%rbp)
movl -4(%rbp), %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size sum, .-sum
.globl main
.type main, @function
main:
.LFB1:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $32, %rsp
movl %edi, -20(%rbp)
movq %rsi, -32(%rbp)
movl $3, %edx
movl $2, %esi
movl $1, %edi
call sum
movl %eax, -8(%rbp)
movl $6, %edx
movl $5, %esi
movl $4, %edi
call sum
movl %eax, -4(%rbp)
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1:
.size main, .-main
.ident "GCC: (Ubuntu 4.8.2-19ubuntu1) 4.8.2"
.section .note.GNU-stack,"",@progbits
gdb中的汇编代码
使用gcc test.c,生成a.out文件。然后gdb a.out进行调试,通过b main,b sum,disassemble等命令。可以得到main,sum函数的汇编代码。
整理后如下:
Dump of assembler code for function main:
0x000000000040050f <+0>: push %rbp
0x0000000000400510 <+1>: mov %rsp,%rbp
0x0000000000400513 <+4>: sub $0x20,%rsp
0x0000000000400517 <+8>: mov %edi,-0x14(%rbp)
0x000000000040051a <+11>: mov %rsi,-0x20(%rbp)
0x000000000040051e <+15>: mov $0x3,%edx
0x0000000000400523 <+20>: mov $0x2,%esi
0x0000000000400528 <+25>: mov $0x1,%edi
0x000000000040052d <+30>: callq 0x4004ed <sum>
0x0000000000400532 <+35>: mov %eax,-0x8(%rbp)
0x0000000000400535 <+38>: mov $0x6,%edx
0x000000000040053a <+43>: mov $0x5,%esi
0x000000000040053f <+48>: mov $0x4,%edi
0x0000000000400544 <+53>: callq 0x4004ed <sum>
0x0000000000400549 <+58>: mov %eax,-0x4(%rbp)
0x000000000040054c <+61>: mov $0x0,%eax
0x0000000000400551 <+66>: leaveq
0x0000000000400552 <+67>: retq
End of assembler dump.
Dump of assembler code for function sum:
0x00000000004004ed <+0>: push %rbp
0x00000000004004ee <+1>: mov %rsp,%rbp
0x00000000004004f1 <+4>: mov %edi,-0x14(%rbp)
0x00000000004004f4 <+7>: mov %esi,-0x18(%rbp)
0x00000000004004f7 <+10>: mov %edx,-0x1c(%rbp)
0x00000000004004fa <+13>: mov -0x18(%rbp),%eax
0x00000000004004fd <+16>: mov -0x14(%rbp),%edx
0x0000000000400500 <+19>: add %eax,%edx
0x0000000000400502 <+21>: mov -0x1c(%rbp),%eax
0x0000000000400505 <+24>: add %edx,%eax
0x0000000000400507 <+26>: mov %eax,-0x4(%rbp)
0x000000000040050a <+29>: mov -0x4(%rbp),%eax
0x000000000040050d <+32>: pop %rbp
0x000000000040050e <+33>: retq
End of assembler dump.
从上面可以看出,整个程序的入口地址应该是0x000000000040050f(如果b main的话,会停在0x0000000000400513)。
调试程序
下面使用gdb调试a.out可执行文件:(中间可能有部分内容就直接省略了,这个都是我事后整理的)
(gdb) b *0x000000000040050f
Breakpoint 1 at 0x40050f
(gdb) run
Starting program: /home/yaoguai/c/a.out
(gdb) p $rsp
$1 = (void *) 0x7fffffffe388
(gdb) ni
0x0000000000400510 in main ()
(gdb) p $rsp
$2 = (void *) 0x7fffffffe380 ;执行push以后,%rsp减到8。这里跟网上减少4不一样,我的机子是64位的。
(gdb) ni
0x0000000000400513 in main () ;执行完mov %rsp,%rbp后,两个寄存器内容一致。
(gdb) p $rsp
$3 = (void *) 0x7fffffffe380
(gdb) p $rbp
$4 = (void *) 0x7fffffffe380
(gdb) ni
0x0000000000400517 in main ()
(gdb) p $rsp
$5 = (void *) 0x7fffffffe360 ;执行%rsi,-0x20(%rbp),大概是给局部变量等分配内存
进入sum函数前一步:
(gdb) ni
0x000000000040052d in main () ;进入sum函数前,查看两寄存器内容
(gdb) p $rsp
$9 = (void *) 0x7fffffffe360
(gdb)
(gdb) p $rbp
$11 = (void *) 0x7fffffffe380
刚好进入sum函数:
(gdb) si
0x00000000004004ed in sum ()
(gdb) p $rsp
$13 = (void *) 0x7fffffffe358 ;这里为什么减2还有待研究
(gdb) p $rbp
$14 = (void *) 0x7fffffffe380 ;未发生改变
0x00000000004004f1 in sum ()
(gdb) p $rsp
$18 = (void *) 0x7fffffffe350 ;执行push后减8
(gdb) p $rbp
$19 = (void *) 0x7fffffffe350 ;%rsp赋值给%rbp
在main中,把三个实参的值存到了三个寄存器中,在sum函数中又从寄存器中读出,并拷贝到内存空间中。
(gdb) ni
0x00000000004004f7 in sum ()
(gdb) x/32x $rbp
0x7fffffffe350: 0xffffe380 0x00007fff 0x00400532 0x00000000
(gdb) x/4x $rbp-0x20
0x7fffffffe330: 0xf7ffe1c8 0x00000003 0x00000002 0x00000001
因为x=1,y=2,z=3。
main中 sum中
mov $0x3,%edx mov %edx,-0x1c(%rbp)
mov $0x2,%esi mov %esi,-0x18(%rbp)
mov $0x1,%edi mov %edi,-0x14(%rbp)
我们知道,在操作系统中,高地址空间是内核代码,低地址是用户空间。结合dump的信息,我们可以画出下面的图来表示当前的内存结构。
kernel space
...........
0x7fffffffe378 0x00000006;main函数中x的值
0x7fffffffe37c 0x0000000c;main函数中y的值
0x7fffffffe380 ;第一次在main函数中%rbp的值
0x7fffffffe358 0x0000000000400532 ;sum中%rbp的值,也是main中call sum下一条指令的地址。
0x7fffffffe350 0x00007fffffffe380 ;上一次%rbp的地址
0x7fffffffe34c 0x00000006 ;sum函数中sum的值
0x7fffffffe330 0xf7ffe1c8 0x00000003 0x00000002 0x00000001;存放xyz的形参值
...........
;main函数的代码,从下往上看
0x0000000000400552 <+67>: retq
0x0000000000400551 <+66>: leaveq
...
0x000000000040052d <+30>: callq 0x4004ed <sum>
0x0000000000400528 <+25>: mov $0x1,%edi
0x0000000000400523 <+20>: mov $0x2,%esi
0x000000000040051e <+15>: mov $0x3,%edx
0x000000000040050f <+0>: push %rbp
..........
;第一次sum求和的情况
0x000000000040050e <+33>: retq
0x000000000040050d <+32>: pop %rbp
...
0x00000000004004f7 <+10>: mov %edx,-0x1c(%rbp) ;z $0x03 0x7fffffffe334
0x00000000004004f4 <+7>: mov %esi,-0x18(%rbp) ;y $0x02 0x7fffffffe338
0x00000000004004f1 <+4>: mov %edi,-0x14(%rbp) ;x $0x01 0x7fffffffe33c
0x00000000004004ee <+1>: mov %rsp,%rbp
0x00000000004004ed <+0>: push %rbp
从上面我们可以看出,main函数通过寄存器传参给sum函数,sum函数中从寄存器读到栈空间,然后进行计算,将结果保存到%eax中。然后main函数再从%eax中读入。而%rbp中存放上一次%rbp的值,其前八个字节存放的是主调函数调用完子函数后即将执行的指令的地址。
总结
- 函数参数可以通过寄存器传参
- 函数参数并不一定是从右向左入栈,查阅资料得知还与编译器有关
- 基址寄存器中,存放的是上一次该寄存器的值
- 基址寄存器前八字节,是主调函数的下一条指令地址
- 代码编译后的机器指令经常被放在代码段里,代码段名为".text";
- 已初始化的全局变量和已初始化的局部静态变量经常放在数据段里,数据段名为".data";
- 未初始化的全局变量和未初始化局部静态变量一般放在“.bss”段里,.bss在文件中不占据空间。
- 字符串常量一般放在“.rodata”段里。