c/c++ 从源码到可执行文件,可执行文件如何运行

以一个例子开始分析:(以下所有实验都是在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;
}


我们都知道c源文件要想编译生成可执行文件,要包括两个主要部分:

1、 编译生成,目标文件(本例中,生成test.o)

2、目标文件,连接生成可执行文件(本例中,生成test)

下面我们生成test.o和test,进行分析:

gcc -c test.c -o test.o
gcc test.o -o test

test.o 和 test中包含的都是二进制码。不严谨的说,test.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下找,再找不到就要报错了。

你可能感兴趣的:(c/c++ 从源码到可执行文件,可执行文件如何运行)