以一个例子开始分析:(以下所有实验都是在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下找,再找不到就要报错了。