编译完成之后,需要的步骤就是 链接.编译仅仅转换源代码到二进制的机器码,但是并没有把程序运行需要的所有资源整合到一起,所以编译后的"目标文件"是没办法直接运行的;在实际的项目中,通常是由多个源代码文件,每个源代码文件都可以进行编译后生成"目标文件“. 这些目标文件 和需要的其他资源被整合到一起,最终才生成我们常见的程序(典型的比如windows下的各种exe文件,linux 下的elf LSB executable 文件,linux 下的elf LSB shared object 等). 这个整合的过程就是“链接”.
以下是用gcc对两个源文件进行编译链接的完整过程示例:
[root@www ~]# cat my.c #源文件my.c
#include<stdio.h>
void myfunction() {
printf("Hello,I am a function, name is : myfunction.\n");
}
[root@www ~]# cat main.c #源文件main.c
#include <stdio.h>
int main() {
myfunction();
printf("Hello,I am the main function!\n");
}
[root@www ~]# gcc -c my.c -o my.obj #编译my.c 为my.obj
[root@www ~]# gcc -c main.c -o main.obj #编译main.c 为main.obj
[root@www ~]# gcc -o my_exe my.obj main.obj # 这一步进行链接操作,如果把“目标文件”换成 源文件,那么编译,链接都在这一条命令里面完成了;
[root@www ~]# ./my_exe #运行最终的可执行文件
Hello,I am a function, name is : myfunction.
Hello,I am the main function!
[root@www ~]#
链接的命令介绍完了,但是要了解程序载入的大致过程,需要对程序的segment head, section head有大概的了解.因为程序加载到内存时候的时候会依赖segment head 记录的信息,而segment head 是在section 的基础上生成的;所以这里进一步了解segment , section.
什么文件才有section , segment 的概念呢? 两者有什么区别?
section 是编译时候生成的,而segment是为了程序加载而存在的概念;segment 通常包含有多个section. 一般有相同属性的section会被安排在同一个segment里面;所以 segment 是section的集合; 对于编译生成的“目标文件”, 是没有segment信息的,但是存在section信息; 链接后的文件既有segment head ,也有section head信息;以/usr/bin/cat 这个程序为例,着重了解下segment的地址怎么计算:
[root@www ~]# readelf -l `which cat` # -l 参数用来查看程序cat 的segment head 信息;
Elf file type is EXEC (Executable file)
Entry point 0x402624
There are 9 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040
0x00000000000001f8 0x00000000000001f8 R E 8
INTERP 0x0000000000000238 0x0000000000400238 0x0000000000400238
0x000000000000001c 0x000000000000001c R 1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x000000000000adcc 0x000000000000adcc R E 200000
LOAD 0x000000000000bc48 0x000000000060bc48 0x000000000060bc48
0x00000000000006d8 0x0000000000001060 RW 200000
DYNAMIC 0x000000000000bde8 0x000000000060bde8 0x000000000060bde8
0x00000000000001d0 0x00000000000001d0 RW 8
NOTE 0x0000000000000254 0x0000000000400254 0x0000000000400254
0x0000000000000044 0x0000000000000044 R 4
GNU_EH_FRAME 0x0000000000009a94 0x0000000000409a94 0x0000000000409a94
0x000000000000030c 0x000000000000030c R 4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 10
GNU_RELRO 0x000000000000bc48 0x000000000060bc48 0x000000000060bc48
0x00000000000003b8 0x00000000000003b8 R 1
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame
03 .init_array .fini_array .jcr .data.rel.ro .dynamic .got .got.plt .data .bss
04 .dynamic
05 .note.ABI-tag .note.gnu.build-id
06 .eh_frame_hdr
07
08 .init_array .fini_array .jcr .data.rel.ro .dynamic .got
上面的结果表示: 一共有9个segments, 其中只有Type为"LOAD" 对应的segments 在程序载入内存的时候会被加载到内存,因此这里只讨论LOAD类型的segment. segment 和section的具体的mapping 关系在上面的结果中有表示; 上述的结果中有一个叫做: VirtAddr的值,这个字段表示 segment对应的“程序虚拟地址”, 也有叫做“文件虚拟地址”的,我觉得都是一个意思:就是说 这个segment 是从这个 虚拟地址开始的,那么结束怎么计算呢? 看FileSiz 字段的值,这个值表示segment 的长度,开始地址加上长度就是结束地址了; 从上面readelf -l 输出的segment head的信息中: 对于cat (/usr/bin/cat)这个程序,编号为02的LOAD的segment 的地址范围是: 0x400000~0x40adcc,这个非常容易理解, 开始地址加上长度就是结束地址, 至此,我们已经获得了其中一个type为LOAD的segment的地址了
但是我们前面已经说了,每个segment 都包含有多个section, 从上面的结果可以看到这个编号为02的segment 对应的section有: .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame ,因此,我们也可以从包含的section 来计算segment 的地址,从而做一个验证: 首先查看cat (/usr/bin/cat) 程序的section 信息如下:
[root@www ~]# readelf -S `which cat`
There are 31 section headers, starting at offset 0xcbd0:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000400238 00000238
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.ABI-tag NOTE 0000000000400254 00000254
0000000000000020 0000000000000000 A 0 0 4
[ 3] .note.gnu.build-i NOTE 0000000000400274 00000274
0000000000000024 0000000000000000 A 0 0 4
[ 4] .gnu.hash GNU_HASH 0000000000400298 00000298
000000000000001c 0000000000000000 A 5 0 8
[ 5] .dynsym DYNSYM 00000000004002b8 000002b8
0000000000000768 0000000000000018 A 6 1 8
[ 6] .dynstr STRTAB 0000000000400a20 00000a20
000000000000031d 0000000000000000 A 0 0 1
[ 7] .gnu.version VERSYM 0000000000400d3e 00000d3e
000000000000009e 0000000000000002 A 5 0 2
[ 8] .gnu.version_r VERNEED 0000000000400de0 00000de0
0000000000000060 0000000000000000 A 6 1 8
[ 9] .rela.dyn RELA 0000000000400e40 00000e40
0000000000000090 0000000000000018 A 5 0 8
[10] .rela.plt RELA 0000000000400ed0 00000ed0
0000000000000690 0000000000000018 AI 5 25 8
[11] .init PROGBITS 0000000000401560 00001560
000000000000001a 0000000000000000 AX 0 0 4
[12] .plt PROGBITS 0000000000401580 00001580
0000000000000470 0000000000000010 AX 0 0 16
[13] .plt.got PROGBITS 00000000004019f0 000019f0
0000000000000008 0000000000000000 AX 0 0 8
[14] .text PROGBITS 0000000000401a00 00001a00
000000000000731a 0000000000000000 AX 0 0 16
[15] .fini PROGBITS 0000000000408d1c 00008d1c
0000000000000009 0000000000000000 AX 0 0 4
[16] .rodata PROGBITS 0000000000408d40 00008d40
0000000000000d53 0000000000000000 A 0 0 32
[17] .eh_frame_hdr PROGBITS 0000000000409a94 00009a94
000000000000030c 0000000000000000 A 0 0 4
[18] .eh_frame PROGBITS 0000000000409da0 00009da0
000000000000102c 0000000000000000 A 0 0 8
[19] .init_array INIT_ARRAY 000000000060bc48 0000bc48
0000000000000008 0000000000000008 WA 0 0 8
[20] .fini_array FINI_ARRAY 000000000060bc50 0000bc50
0000000000000008 0000000000000008 WA 0 0 8
[21] .jcr PROGBITS 000000000060bc58 0000bc58
0000000000000008 0000000000000000 WA 0 0 8
[22] .data.rel.ro PROGBITS 000000000060bc60 0000bc60
0000000000000188 0000000000000000 WA 0 0 32
[23] .dynamic DYNAMIC 000000000060bde8 0000bde8
00000000000001d0 0000000000000010 WA 6 0 8
[24] .got PROGBITS 000000000060bfb8 0000bfb8
0000000000000030 0000000000000008 WA 0 0 8
[25] .got.plt PROGBITS 000000000060c000 0000c000
0000000000000248 0000000000000008 WA 0 0 8
[26] .data PROGBITS 000000000060c260 0000c260
00000000000000c0 0000000000000000 WA 0 0 32
[27] .bss NOBITS 000000000060c320 0000c320
0000000000000988 0000000000000000 WA 0 0 32
[28] .gnu_debuglink PROGBITS 0000000000000000 0000c320
0000000000000010 0000000000000000 0 0 4
[29] .gnu_debugdata PROGBITS 0000000000000000 0000c330
0000000000000780 0000000000000000 0 0 1
[30] .shstrtab STRTAB 0000000000000000 0000cab0
000000000000011e 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
[root@www ~]#
从上述的结果中可以看到: .interp 这个section的开始地址是 :0x400238, .en_frame的结束地址是: 0x409da0+0x102c=0x40adcc , 所以通过section的分析 发现编号为2的segment 地址应该是: 0x400238~0x40adcc ; 到这里总结一下上面两个方式得到的同一个segment( /usr/bin/cat 这个程序的第一个load segment)的地址: 直接读取程序头的segment ,获得的地址是: 0x400000~0x40adcc 通过计算segment所包含的section, 获得的地址是: 0x400238~0x40adcc
为什么上述两种方式得到的同一个segment的地址会有偏差呢?
因为程序虚拟地址空间的分配是 以page为单位的,而每个page的大小默认为4KB. 如果segment 的开始地址不是在page的开头,结束地址不是在page的结尾,那么这两个地址都需要 进行page 对齐的调整;所以对上述的地址按照page对齐(4KB对齐就是 以0x1000为单位进行对齐)进行调整,调整后结果如下: 0x400000~0x40adcc ----->0x400000~0x40b000 0x400238~0x40adcc ----->0x400000~0x40b000 所起,无论是从segment head读取的地址,还是通过section 计算出来的地址,通过 page 对齐调整后,都是同一个地址;