1.C语言创建程序
1.1C语言创建(分为4个步骤)
- 编辑
- 编译
- 链接
- 执行
编辑:就是创建和修改C程序的源代码-我们编写的程序称为源代码。
编译:就是将源代码转换为机器语言。编译器的输出结果成为目标代码,存放它们的文件称为目标文件。扩展名为.o或者.obj。
(该部分编译是指汇编器编译汇编语言或者编译器编译高级语言)
链接:链接器将源代码由编译器产生的各种模块组合起来,再从C语言提供的程序库中添加必要的代码模块,将它们组成一个可执行的文件。在windows下扩展名为.exe,Unix下无扩展名。
执行:运行程序。
1.2什么是源代码,目标文件,可执行文件。
源代码 ——源文件就是存放程序代码的文件。通常我们编辑代码的文件就是源文件。
- 源代码相对目标代码和可执行代码而言的。
- 源代码就是用汇编语言和高级语言写出来的地代码。
目标文件——指源代码经过编译程序产生的能被cpu直接识别二进制代码。
- 目标代码指计算机科学中编译器或汇编器处理源代码后所生成的代码,它一般由机器代码或接近于机器语言的代码组成。
- 目标文件包括着机器代码(可直接被计算机中央处理器履行)和代码在运行时使用的数据,如重定位信息,如用于链接或调试的程序符号(变量和函数的名字),另外还包括其他调试信息。
gcc -c main.c
编译main.c ,生成目标文件main.o,但不进行link.
gcc -o main.o
链接成可执行文件main。
可执行文件——可执行代码就是将目标代码连接后形成的可执行文件,当然也是二进制的。 连接程序系统库文件连接就生成可执行文件。
例如:*.obj是程序编译之后生成的目标文件,连接程序再将这个文件与系统库文件连接就生成可执行文件
根据上面的图,我们可以看到链接器还额外链接了2个部分。
目标代码文件中所缺少的第一个元素是一种叫做启动代码(Start-up code)的东西,此代码相当于您的程序和操作系统之间的接口。例如你可以在dos 或Linux下运行一个 IBM PC 兼容机,在两种情况中硬件是相同的,所以都会使用同样的目标代码,但是 DOS与Linux要使用不用的启动代码,因为这两种系统处理程序的方式不同的。
所缺少的第二个元素是库例程的代码。几乎所有C程序都利用标准库中所包含的例程(称为函数)。例如,程序中的函数printf()。目标代码文件不包含这一函数的指令。实际代码存储在另一个称为“库”的文件中,库文件中包含许多函数的目标代码。
链接器的作用是将这3个元素(目标代码、系统的标准启动代码和库代码)结合在一起,并将他们存放在单个文件,即可执行文件中。对库代码来说,链接器只从库中提取您所使用的函数所需的代码。
可以得出结论:目标文件和可执行文件都是由机器语言指令组成的。但目标文件只包含您所编写的代码转换成的机器语言,而可执行文件还包含您所使用的库例程以及启动代码。
下面这幅图能大致说明一下链接的情况。
这是一个main.o目标代码,内部有main,foo,bar三个函数。
U main表示main这个符号在crtl1.o中用到了,但是没有定义。因此需要main.o提供定义并和crtl1.o链接在一起。main整个程序的入口实际上是_crtl1.o中的 _start,它做了一些初始化工作(启动历程),然后调用C代码中提供的main.c函数。libc是运行时候动态链接libc共享库(库中包含常用的函数)。
所以程序的入口点其实是_start,main函数实际上是被_start调用。
1.4gcc命令图
2.ELF文件(该部分分析目标文件和可执行文件的,涉及部分汇编指令)
ELF文件格式是一个开放标准,各种UNIX系统的可执行文件都采用ELF格式,它有三种不同的类型:
- 可重定位的目标文件
- 可执行文件
- 共享库
ELF文件格式提供了两种不同的视角,在汇编器和链接器看来,ELF文件是由Section HeaderTable描述的一系列Section的集合,而执行一个ELF文件时,在加载器(Loader)看来它是 由Program Header Table描述的一系列Segment的集合。如下图所示。
左边是从汇编器和链接器的视角来看这个文件,开头的ELF Header描述了体系结构和操作系统 等基本信息,并指出Section Header Table和Program Header Table在文件中的什么位 置,Program Header Table在汇编和链接过程中没有用到,所以是可有可无的,Section Header Table中保存了所有Section的描述信息。右边是从加载器的视角来看这个文件,开头 是ELF Header,Program Header Table中保存了所有Segment的描述信息,Section Header Table在加载过程中没有用到,所以是可有可无的。注意Section Header Table和ProgramHeader Table并不是一定要位于文件开头和结尾的,其位置由ELF Header指出,上图这么画只是为了清晰。
目标文件需要链接器做进一步处理,所以一定有Section Header Table;可执行文件需要加载运行,所以一定有Program Header Table;而共享库既要加载运行,又要在加载时做动态链接, 所以既有Section Header Table又有Program Header Table。
- section:C语言内存中的.text,.data,.bss.....
- Segment:是指在程序运行时加载到内存的具有相同属性的区域,由一个或多个Section组成,比如有两个Section都要求加载到内存后可读可写,就属于同一个Segment。有些Section只对汇编器和链接器有意义,在运行时用不到,也不需要加载到内 存,那么就不属于任何Segment 。
2.1重定位目标文件
在进行该部分之前,我们先查看一下网上的部分重定位目标文件的资料。
资料一:
汇编器所产生的目标文件至少包括三个区,即文本区(text),数据区(data)和bss区。文本区一般包括程序的代码和常量,数据区通常存放全局变量等内容,bss区用于存放未初始化的变量或作为公共变量存储空间。在一个目标文件中,其text区从地址0开始,随后是data区,再后面是bss区。而要运行程序,必须装载到内存中,所以这些区的地址需要在内存中重新安排,也就是重定位。
资料二:
编译器编译后产生的目标文件是可重定位的程序模块,并不能直接运行,链接就是把目标文件和其他分别进行编译生成的程序模块(如果有的话)及系统提供的标准库函数连接在一起,生成可运行的可执行文件的过程。
重定位是链接器在完成符号解析后(知道了各个输入模块的代码段和数据段的大小)的一个步骤,其作用顾名思义就是重新定位,确定比如指令,全局变量等在运行时的存储器地址。
资料三:
比如说两个编译后的可重定位目标文件obj1.o和obj2.o
在obj1.o里面定义了一个全局变量glob(在obj1里面记录了glob相对于该文件数据段的相对地址), 而obj2.0里面又引用了这个全局变量glob。
链接的重定位就是要确定在链接后的可执行程序中glob的地址,而不是相对于obj1的地址,从而使obj2也能通过地址调用glob。
当然重定位并不只是全局变量,还包括外部函数,指令等运行时地址的确定
资料四:
当你在程序中写上一个全局变量或者是一个函数时,这个定位过程会经历几个阶段:
1.在这个目标文件中的相对定位,一个目标文件中对此文件中的所有函数,变量进行符号描述,比如一个变量A,它所占的相对地址是多少?是全局的?或者是静态的,或者是外部的??
2.在连接多个目标成一个可执行文件时,会再次对这个变量进行重定位,也就是在这个可执行文件中进行对此变量进行描述,同目标文件中的描述差不多,只不过此变量不再有外部,内部之分,都成了本地变量,并且会将所有全局变量存放在一定的逻辑地址中,这是通过连接脚本文件与各个目标文件中的相对地址共同决定的
3.最终的操作系统加载这个可执行文件时,会对这些变量与函数地址再次进行重定位,其方式就是首先分析这个可执行文件中的不同段,读出相应的描述表,然后通过逻辑地址与物理地址进行映射出,最终就将可执行的二进制码加进了真实的物理内存了,关于分析可执行文件格式与物理地址的转换,不同的CPU与操作系统的实现方式会有不同之处
接下来我们开始实践部分,首先写一个求一组数的最大值的汇编程序max.s。
现在有一个max.o目标文件,我们用readlf工具读取其ELF Header和Section Header Table
ELF Header中描述了操作系统是UNIX,体系结构是80386。Section Header Table中有8个Section Header,从文件地址200
(0xc8)开始,每个Section Header占40字节,共320字节,到文件地址0x207结束。这个目标文件没有Program Header。文件地址是这样定义的:文件开头第一个字节的地址是0,然后每个字节占一个地址。
从Section Header中读出各Section的描述信息。
Addr是这些段加载到内存中的地址(程序中的地址都是虚拟地址),加载地址要在链接时填写,现在空缺,所以是全0。
Off和Size两列指出了各Section的文件地址,比如.data段从文件地址0x60开始,一共0x38个字节,回去翻一下程序,.data段定义了14个4字节的整数,一共是56个字节,也就是0x38。
根据以上信息可以描绘出整个目标文件的布局。
** Section Header Table**:读出各Section的描述信息。
.shstrtab:保存着各Section的名字,比如.text,.data.....。
.strtab:保存着程序中用到的符号的名字.比如汇编程序的start_loop:和loop_exit符号。(对应的就是for循环)。
**.data **:保存程序中已初始化的全局变量和静态变量以及字符串常量。
.bss:存放程序中未初始化的全局变量和静态变量。
.text:存放程序执行代码。
.rel.text:告诉链接器指令中的哪些地方需要做重定位。
下节分析。
我们看一下.text段内容
text段代码中,一些跳转指令和内存访问指令中的地址都是符号的相对地址,下一步链接器要修改这些指令,把其中的地址都改成加载时的内存地址,这些指令才能正确执行。
2.2可执行文件
现在分析可执行文件max。
在ELF Header中,Type改成了EXEC,由目标文件变成可执行文件了多了两个Program Header,少了两个Section Header。
在Section Header Table中,.text和.data的加载地址分别改成了0x0804 8074和0x0804 90a0。.bss段没有用到,所以被删掉了。.rel.text段就是用于链接过程的,链接完了就没用 了,所以也删掉了。
多出来的Program Header Table描述了两个Segment的信息。.text段和前面的ELF Header、Program Header Table一起组成一个Segment(FileSiz指出总长度 是0x9e),.data段组成另一个Segment(总长度是0x38)。VirtAddr列指出第一 个Segment加载到虚拟地址0x0804 8000,第二个Segment加载到地址0x0804 90a0。Flg列指出第一个Segment的访问权限是可读 可执行,第二个Segment的访问权限是可读可写。
原来目标文件符号表中的Value都是相对地址,现在都改成绝对地址了。
我们查看一下.txt段内容。
现在我们对比一下目标文件和可执行文件的不同。
目标文件.text和.data段地址
可执行文件.text和.data段地址
目标文件中跳转指令
可执行文件中跳转指令
目标文件中内存访问指令
可执行文件中内存访问指令
- 可以看到指令中的相对地址都改成绝对地址了。
- 结合上2部分分析,我们可以看到。
- .text和.data段代码加载到内存中的地址由空缺0变成了具体地址。
- .text段代码中一些跳转指令和内存访问指令中的地址由相对地址改成加载时的内存地址,
- .data段代码也由相对地址改为绝对地址。
3.静态库和共享库
库:有时候需要把一组代码编译成一个库,这个库在很多项目中都要用到,例如libc就是这样一个库,我们在不同的程序中都会用到libc中的库函数(例如printf)。
共享库和静态库的区别:在链接libc共享库时只是指定了动态链接器和该程序所需要的库文件,并没有真的做链接,可执行文件调用的libc库函数仍然是未定义符号,要在运行时做动态链接。而在链接静态库时,链接器会把静态库中的目标文件取出来和可执行文件真正链接在一起。
- 静态库链接后,指令由相对地址变为绝对地址,各段的加载地址定死了。
- 共享库链接后,指令仍是相对地址,共享库各段的加载地址并没有定死,可以加载到任意位置。
静态库好处:静态库中存在很多部分,链接器可以从静态库中只取出需要的部分来做链接 (比如main.c需要stach.c其中的一个函数,而stach.c中有4个函数,则打包库后,只会链接用到那个函数)。另一个好处就是使用静态库只需写一个库文件名,而不需要写一长串目标文件名