首页 > 代码库 > 【UNIX】从一个可执行文件的生成到进程在内存中分布 (上)
【UNIX】从一个可执行文件的生成到进程在内存中分布 (上)
一个源程序要生成可执行文件,需要通过编译器来实现,而编译器所起的作用就是把程序员编写的高级语言翻译成机器能读懂的低级语言(二进制代码),这里编译器起到翻译的作用。在Linux操作系统中采用的是GCC交叉编译工具。
虽然说GCC是个编译器,但是使用GCC从一个C语言程序生成可执行文件不仅仅只是编译过程,而是要经过四个相互关联的步骤:预处理(也称预编译,preprocessing)、编译(comlilation)、汇编(assembly)和链接(linking)
第一步:预处理
Gcc -E test.c -o test.i // -o(小欧)表示目标文件objective目标;O(大欧)表示优化系数
#include<stdio.h>
int main()
{
printf("hello kano");
return 0;
}
预处理之后
typedef int (*__gconv_trans_fct) (struct __gconv_step *,
struct __gconv_step_data *, void *,
__const unsigned char *,
__const unsigned char **,
__const unsigned char *, unsigned char **,
size_t *);
…
# 2 "test.c" 2
int main()
{
printf("hello kano ");
return 0;
}
通上面的代码可以看出来在预处理阶段GCC编译器只把源文件里的头文件合并以及宏定义替换到源文件中,没有进行代码的编译
第二步:编译
Gcc -S test.i -o test.s
.file "test.c"
.section .rodata
.align 4
.LC0:
.string"hello kano"
.text
.globl main
.type main,
main:
Pushl p
movl %esp, p
subl $8, %esp
此时已经由C语言编程了汇编语言了
第三步:汇编
Gcc -c test.s -o hello.o
编译器这里就已经完成了C语言到机器码的编译过程,并生成了目标文件
第四步:链接
Gcc testo -o test
要想一个程序功能的完整实现还需要一些库函数的加载之后把各个目标文件模块整合为一个可执行文件。此时我们会主要到哪些printf标准库函数是怎么被调用以及加载到我们的源程序的?在预编译中包含进的”stdio.h”中也只有该函数的声明,而没有定义函数的实现,系统把这些函数实现都被做到名为libc.so.6的库文件中去了,在没有特别指定时,Gcc会到系统默认的搜索路径”/usr/lib”下进行查找,也就是链接到libc.so.6库函数中去,这样就能实现函数”printf”了,而这也就是链接的作用。
函数库一般分为静态库和动态库两种。静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了。其后缀名一般为”.a”。动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库,这样可以节省系统的开销。动态库一般后缀名为”.so”,如前面所述的libc.so.6就是动态库。Gcc在编译时默认使用动态库。
完成了链接之后,Gcc就可以生成可执行文件
附录:
.c为后缀的文件,C语言源代码文件;
.a为后缀的文件,是由目标文件构成的档案库文件;
.C,.cc或.cxx 为后缀的文件,是C++源代码文件;
.h为后缀的文件,是程序所包含的头文件;
.i 为后缀的文件,是已经预处理过的C源代码文件;
.ii为后缀的文件,是已经预处理过的C++源代码文件;
.m为后缀的文件,是Objective-C源代码文件;
.o为后缀的文件,是编译后的目标文件;
.s为后缀的文件,是汇编语言源代码文件;
.S为后缀的文件,是经过预编译的汇编语言源代码文件。
【UNIX】从一个可执行文件的生成到进程在内存中分布 (上)