以一个例子开始分析:(以下所有实验都是在linux下完成)
//test.c int g_num=2000; char g_string[10000]="hello c"; int multi(int a,int b){ int result= a*b; int dummy[10000]; return result; } int main(){ multi(5,8); return 0; }
1、 编译生成,目标文件(本例中,生成test.o)
2、目标文件,连接生成可执行文件(本例中,生成test)
下面我们生成test.o和test,进行分析:
gcc -c test.c -o test.o gcc test.o -o test
代码段,就是cpu需要执行的每一条指令;而,数据段,就是我们通常说的,静态存储区(不同于堆栈)。
要想详细的查看test或test.o中包含了什么,需要将test.c 转换为汇编代码查看,下面,我们将生成汇编代码:
gcc -S test.c -o test.s
.file "test.c" .globl g_num .data .align 4 .type g_num, @object .size g_num, 4 g_num: .long 2000 .globl g_string .align 32 .type g_string, @object .size g_string, 10000 g_string: .string "hello c" .zero 9992 .text .globl multi .type multi, @function multi: .LFB0: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 subq $3904, %rsp movl %edi, -4020(%rbp) movl %esi, -4024(%rbp) movl -4020(%rbp), %eax imull -4024(%rbp), %eax movl %eax, -4004(%rbp) movl -4004(%rbp), %eax leave .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: .size multi, .-multi .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 movl $8, %esi movl $5, %edi call multi movl $0, %eax popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE1: .size main, .-main .ident "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.1) 4.8.4" .section .note.GNU-stack,"",@progbits
下面我们看一些通常在c中说的概念:
全局变量(以及静态变量):在代码中对应 g_num. g_string 在汇编代码中的数据段(data segment)。
局部变量: 在函数中定义的变量,在栈中,(本例不太明显,主要原因是multi是该程序调用的最后一个函数,所以编译器做了优化),如果该函数中,再调用另一个函数,会看到函数先将sp减小,即是分配了局部变量的空间。
在教科书中,函数调用时,会将参数按相反的方向push到栈中,然后,把返回地址push到栈中,然后进入函数,进入函数后,将bp,push到栈中,然后将sp赋值到bp。下面进行计算,计算完毕,将bp,pop出来,再把返回地址pop出来,返回到调用函数,然后,将sp恢复。返回值是通过寄存器传递的,多是eax。
返回值是如何传递的呢? 通常返回值是通过寄存器传递eax 或 edx等,但是,如果返回的是一个非常大的对象,将如何实现呢? 如果返回的是一个非常大的对象,caller 函数将把返回的地址传递到edi寄存器,called函数,将把计算的返回值填充到edi地址内,这样就实现了函数返回值的传递。
查看test.o 和 test 两个文件:可以看出: test.o 中包含函数名字 multi (虽然是二进制的),test中不包含函数名字,但是可以看到/usr/lib/libc.so的字样。
这个对于之后,我们理解连接过程非常有帮助。(注意:用g++编译,函数名字会加上一些前缀和后缀)
连接过程 : 其实就是将多个目标文件合并起来,从main函数开始,依次找到需要的函数名(是真正的字符串函数名,我们之前看到.o文件中包含函数名),如果找不到函数名,就要到动态链接库中去找了,gcc默认的动态链接库的目录是/lib,如果找不到,就鸡鸡了。当然,可以加上编译选项, —Lx即加入搜索路径。如果所有函数都找到,就会编译成功,生成可执行文件。
但是,编译成功,与可执行文件可以运行(即使代码都是正确的)是两件事,虽然,往往编译成功,可执行文件就可以运行。
在命令行中,输入可执行文件的命令:
首先先要做地址映射,将代码段和数据段分别映射到虚拟内存,以及物理内存中。这个阶段,完成了全局变量的空间分配(包括静态变量)。
然后,就是一条指令一条指令的执行了,执行中有时候会遇到找不到的函数,这时就需要寻找动态链接库,动态链接库的寻找方法是,现在LD_LIBRARY_PATH下找,找不到就到/etc/ld.so.conf.d下找,再找不到就要报错了。