往期地址:
本期主题:
在平常的应用程序开发中,我们很少关注编译和链接过程,这是因为通常的开发环境(IDE)都已经将这些给集成好了,我们只需要build即可,通常这种将编译和链接合并在一起的过程称为构建(build)。
但是在这样的开发过程中,我们往往会被这些复杂的集成工具所提供的强大功能所迷惑,很多软件的运行机制和原理我们不是很理解,这样在遇到问题时,经常束手无策。
实际上一个简单的hello.c,在linux下使用gcc编译时,也可以分为4个步骤:
首先是源代码hello.c和相关的头文件,如stdio.h被预编译器cpp预编译成一个.i文件
$ gcc -E hello.c -o hello.i
目的
:处理源代码中的以 “#” 开始的预编译指令,如"#include"、"#define"等处理规则
:
结果
:经过预编译后生成的.i文件不含任何的宏定义,并且包含的文件也被插入到.i文件中,所以当我们无法判断宏展开的结果以及头文件包含是否正确时,可查看预编译后的文件来定位问题把预处理完的文件进行一系列词法分析、语法分析、语义分析后生成相应的汇编代码文件。
gcc -S hello.i -o hello.s
目的
:将预编译后的文件进行一系列语法分析后产生相应的汇编代码文件。程序
:和预编译一样使用cc1汇编器实际上是将汇编代码文件转变成可以被机器所执行的指令。
gcc -c hello.s -o hello.o
生成的.o文件是目标文件(object file),这一步生成的文件并不是一个可执行文件,需要将很多东西链接起来,才是可执行文件
。
文件属性:
jason@ubuntu:~/WorkSpace/0.Unix_AP/compile$ gcc -c hello.c -o test2
jason@ubuntu:~/WorkSpace/0.Unix_AP/compile$ file test2
test2: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
带着问题阅读本节,为什么在汇编这一步不直接生成一个可执行文件呢,而要再多这一步链接?
程序设计的模块化一直是程序员所追求的目标,人们将每个模块的代码独立编译,然后按照某种规则将他们给组装起来,这个组装的过程就是链接
。
链接的过程主要包括:
基本的静态链接如上图所示:每个模块的源文件经过编译器编译生成目标文件(.o),目标文件和库一起链接形成最终可执行文件。
现在的链接器空间分配策略一般都采用两步链接的方式,整个链接过程被分为两步,分别是:
下面来详细说说。
过程:
目标文件中的符号表
,统一放到一个全局符号表中链接的第二步操作主要是,使用上一步收集到的信息,然后读取输入文件中的数据以及重定位信息,进行符号解析与重定位,调整代码中的地址。
事实上,第二步是整个链接过程的核心,特别是重定位过程
。
这里来看一个实际例子,假设我们的代码中有a.c和b.c两个文件
//a.c
#include "stdio.h"
extern int glb_shared;
int main(void)
{
int a = 100;
swap(&a, &glb_shared);
}
//b.c
#include "stdio.h"
int glb_shared = 1;
void swap(int* a, int* b)
{
*a = *b;
}
在链接前生成目标文件:
$ gcc -c -fno-stack-protector a.c b.c
//这里需要使用 -fno-stack-protector选项的原因是 fno-stack-protector是去除了stack的检测,我们直接是手动裸ld去链接,没有链接到“__stack_chk_fail"的所在库,不然会报错 ld: a.o: in functionmain': a.c:(.text+0x4f): undefined reference to
__stack_chk_fail’链接后生成目标文件
$ ld a.o b.o -e main -o ab
链接前后的目标文件对比:
读取连接前目标文件信息
$ objdump -h a.o
a.o: 文件格式 elf64-x86-64
节:
Idx Name Size VMA LMA File off Algn
0 .text 00000032 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000000 0000000000000000 0000000000000000 00000072 2**0
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 0000000000000000 0000000000000000 00000072 2**0
...
$ objdump -h b.o
b.o: 文件格式 elf64-x86-64
节:
Idx Name Size VMA LMA File off Algn
0 .text 0000001f 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 00000004 0000000000000000 0000000000000000 00000060 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 0000000000000000 0000000000000000 00000064 2**0
...
读取链接后目标文件:
$ objdump -h ab.o
ab.o: 文件格式 elf64-x86-64
节:
Idx Name Size VMA LMA File off Algn
0 .note.gnu.property 00000020 00000000004001c8 00000000004001c8 000001c8 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
1 .text 00000051 0000000000401000 0000000000401000 00001000 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
2 .eh_frame 00000058 0000000000402000 0000000000402000 00002000 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
3 .data 00000004 0000000000404000 0000000000404000 00003000 2**2
可以看到的信息:
前面说到了,链接之后ab中的各个段被分配到了相应的虚拟地址,但是想一下,链接器是怎么知道哪个变量被调整呢?
这是因为在ELF文件中,有一个重定位表(relocation table)
,这个表专门用来保存与重定位相关的信息,我们可以使用 objdump 来查看目标文件的重定位表:
$ objdump -r a.o
a.o: 文件格式 elf64-x86-64
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
000000000000001a R_X86_64_PC32 glb_shared-0x0000000000000004
0000000000000027 R_X86_64_PLT32 swap-0x0000000000000004
RELOCATION RECORDS FOR [.eh_frame]:
OFFSET TYPE VALUE
0000000000000020 R_X86_64_PC32 .text
能看到"a.o"中需要进行重定位的地方,即a.o所引用到的所有外部符号都有一个重定位地址。
也可使用 readelf命令 来查看各个目标文件的符号表:
//链接前
$ readelf -s a.o
Symbol table '.symtab' contains 13 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS a.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 6
6: 0000000000000000 0 SECTION LOCAL DEFAULT 7
7: 0000000000000000 0 SECTION LOCAL DEFAULT 8
8: 0000000000000000 0 SECTION LOCAL DEFAULT 5
9: 0000000000000000 50 FUNC GLOBAL DEFAULT 1 main
10: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND glb_shared
11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND swap
//链接后
$ readelf -s ab.o
Symbol table '.symtab' contains 14 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000004001c8 0 SECTION LOCAL DEFAULT 1
2: 0000000000401000 0 SECTION LOCAL DEFAULT 2
3: 0000000000402000 0 SECTION LOCAL DEFAULT 3
4: 0000000000404000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 0 FILE LOCAL DEFAULT ABS a.c
7: 0000000000000000 0 FILE LOCAL DEFAULT ABS b.c
8: 0000000000401032 31 FUNC GLOBAL DEFAULT 2 swap
9: 0000000000404004 0 NOTYPE GLOBAL DEFAULT 4 __bss_start
10: 0000000000401000 50 FUNC GLOBAL DEFAULT 2 main
11: 0000000000404000 4 OBJECT GLOBAL DEFAULT 4 glb_shared
12: 0000000000404004 0 NOTYPE GLOBAL DEFAULT 4 _edata
13: 0000000000404008 0 NOTYPE GLOBAL DEFAULT 4 _end
能看到在链接前,a.o的符号表中提示引用符号 glb_shared以及swap都是undefined,所以链接时也需要其他的目标文件来定义这些外部符号,而链接后的ab.o文件就有了这些符号的定义。
所以像我们常见的错误(undefined reference to xxxxx ):
$ ld a.o
ld: 警告: 无法找到项目符号 _start; 缺省为 0000000000401000
ld: a.o: in function `main':
a.c:(.text+0x1a): undefined reference to `glb_shared'
ld: a.c:(.text+0x27): undefined reference to `swap'
这些错误的根本原因是:
链接器会查找所有输入目标文件的符号表,组成一个全局符号表,然后进行重定位。上述错误在于没找到符号表
链接器一般都提供多种可以用来控制整个链接过程的方法,一般有以下几种方式:
当我们不指定链接脚本时,也会使用到默认的链接脚本,可以使用如下命令来查询默认链接脚本:
$ ld -verbose
GNU ld (GNU Binutils for Ubuntu) 2.34
支持的仿真:
elf_x86_64
elf32_x86_64
elf_i386
elf_iamcu
elf_l1om
elf_k1om
i386pep
i386pe
使用内部链接脚本:
==================================================
/* Script for -z combreloc -z separate-code */
/* Copyright (C) 2014-2020 Free Software Foundation, Inc.
Copying and distribution of this script, with or without modification,
are permitted in any medium without royalty provided the copyright
notice and this notice are preserved. */
OUTPUT_FORMAT("elf64-x86-64", "elf64-x86-64",
"elf64-x86-64")
OUTPUT_ARCH(i386:x86-64)
ENTRY(_start)
SEARCH_DIR("=/usr/local/lib/x86_64-linux-gnu"); SEARCH_DIR("=/lib/x86_64-linux-gnu"); SEARCH_DIR("=/usr/lib/x86_64-linux-gnu"); SEARCH_DIR("=/usr/lib/x86_64-linux-gnu64"); SEARCH_DIR("=/usr/local/lib64"); SEARCH_DIR("=/lib64"); SEARCH_DIR("=/usr/lib64"); SEARCH_DIR("=/usr/local/lib"); SEARCH_DIR("=/lib"); SEARCH_DIR("=/usr/lib"); SEARCH_DIR("=/usr/x86_64-linux-gnu/lib64"); SEARCH_DIR("=/usr/x86_64-linux-gnu/lib");
......
常见的命令语句如下:
命令语句 | 说明 |
---|---|
ENTRY(symbol) | 指定符号symbol为入口地址,ld有多种方法指定入口地址,优先级关系如下:ld命令行的 -e 选项 > 链接脚本的ENTRY(symbol)命令 > _start符号值 > .text段的第一字节地址 > 使用值0 |
STARTUP(filename) | 将文件filename作为链接过程的第一个输入文件 |
SEARCH_DIR(path) | 将路径path加入到链接器的库查找目录里 |
INPUT(file,file,…) | 将指定文件作为链接过程的输入文件 |
SECTION命令最基本格式为:
SECTIONS
{
....
secname : { contents }
...
}
contents规则,条件写法如下:
filename(sections)
其中,filename表示输入文件名,section表示段名,看几个具体例子: