1 揭开链接器的面纱
- 问题
- 源文件被编译后生成目标文件,这些目标文件如何生成最终的可执行程序?
- 链接器的意义
- 链接器的主要作用是把各个模块之间相互引用的部分处理好,使得各个模块之间能够正确的衔接
目标文件的秘密
- 各个段没有具体的地址,只有段大小信息
- 各个标识符没有实际的地址,只有段中的相对地址
- 段和标识符的实际地址需要链接器具体确定
链接器工作的内容:将目标文件和库文件整合为最终的可执行程序
- 合并各个目标文件中的段(.text,.data,.bss)
- 确定各个段和段中标识符的最终地址(重定位)
2 gcc下程序的执行流程
问题:main()函数是第一个被调用执行的函数吗?
2.1 默认情况下(gcc)
1.程序加载后,_start()是第一个被调用执行的函数。
2._start()函数准备好参数后立即调用_lib_start_main()函数。
3._lib_start_main()初始化运行环境后调用main()函数执行(main()函数在_lib_start_main()函数中执行)。
_start()函数的入口地址就是代码段(.text)的起始地址!
_lib_start_main()函数的作用
1.调用_libc_csu_init()函数(完成必要的初始化)。
2.启动程序的第一个线程(主线程),main()为线程入口。
3.注册_lib_csu_fini()函数(程序运行终止时被调用)。
程序的启动过程
2.2 自定义程序入口函数
gcc提供-e选项用于在连接时指定入口函数。
自定义入口函数时必须使用-nostartfiles选项进行链接
#include <stdio.h>
#include <stdlib.h>
int program(void)
{
printf("this is main...\n");
exit(0);
}
执行的链接命令如下:
gcc -c main.c -o main.o
gcc -e program main.o -nostartfiles -o main.out
./a.out
this is main...
思考:
链接选项-nostartfiles的意义是什么?
链接器根据什么原则完成具体的工作?
3 链接脚本
思考:连接器根据什么原则完成具体的工作?
3.1 链接脚本的概念和意义
概念:链接脚本用于描述链接器处理目标文件和库文件的方式。
意义:
合并各个目标文件中的段。
重定位各个段的起始地址。
重定位各个符号的最终地址。
3.2 链接脚本的本质
3.3 链接脚本初探
注意事项:
各个段的链接地址必须符合具体平台的规范。在Linux中,进程代码段(.text)的合法起始地址为[0x08048000,0x08049000]。
链接脚本中能够直接定义标识符并指定存储地址。
链接脚本中能够指定源代码中标识符的存储地址。
链接脚本初体验
test.c:
#include <stdio.h>
int s1;
extern int s2;
int main(void)
{
printf("&s1 = %p.\n", &s1);
return 0;
}
SECTIONS
{
.text 0x10000000:
{
*(.text)
}
. = 0x40000000;
S = .;
.data :
{
*(.data)
}
.bss :
{
*(.bss)
}
}
注意:并不会因为改变位置指针就会影响后面段的起始地址,只有强制指定段地址才会影响后续段的地址。
gcc test.c test.lds
使用objdump -h a.out的结果如下:
a.out: file format elf32-i386
Sections:
Idx Name Size VMA LMA File off Algn
0 .interp 00000013 08048174 08048174 00000174 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
1 .note.ABI-tag 00000020 08048188 08048188 00000188 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .note.gnu.build-id 00000024 080481a8 080481a8 000001a8 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
3 .gnu.hash 00000020 080481cc 080481cc 000001cc 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .dynsym 00000050 080481ec 080481ec 000001ec 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
5 .dynstr 0000004c 0804823c 0804823c 0000023c 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
6 .gnu.version 0000000a 08048288 08048288 00000288 2**1
CONTENTS, ALLOC, LOAD, READONLY, DATA
7 .gnu.version_r 00000020 08048294 08048294 00000294 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
8 .rel.dyn 00000008 080482b4 080482b4 000002b4 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
9 .rel.plt 00000018 080482bc 080482bc 000002bc 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
10 .init 00000023 080482d4 080482d4 000002d4 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
11 .plt 00000040 08048300 08048300 00000300 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
12 .text 000001a2 10000000 10000000 00001000 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
13 .fini 00000014 100001a4 100001a4 000011a4 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
14 .rodata 00000013 100001b8 100001b8 000011b8 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
15 .eh_frame_hdr 0000002c 100001cc 100001cc 000011cc 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
16 .eh_frame 000000b0 100001f8 100001f8 000011f8 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
17 .init_array 00000004 10001f08 10001f08 00001f08 2**2
CONTENTS, ALLOC, LOAD, DATA
18 .fini_array 00000004 10001f0c 10001f0c 00001f0c 2**2
CONTENTS, ALLOC, LOAD, DATA
19 .jcr 00000004 10001f10 10001f10 00001f10 2**2
CONTENTS, ALLOC, LOAD, DATA
20 .dynamic 000000e8 10001f14 10001f14 00001f14 2**2
CONTENTS, ALLOC, LOAD, DATA
21 .got 00000004 10001ffc 10001ffc 00001ffc 2**2
CONTENTS, ALLOC, LOAD, DATA
22 .got.plt 00000018 10002000 10002000 00002000 2**2
CONTENTS, ALLOC, LOAD, DATA
23 .data 00000008 10002018 10002018 00002018 2**2
CONTENTS, ALLOC, LOAD, DATA
24 .bss 00000014 10002020 10002020 00002020 2**2
ALLOC
25 .comment 0000004f 00000000 00000000 00002020 2**0
CONTENTS, READONLY
SECTIONS
{
.text 0x08048400:
{
*(.text)
}
. = 0x01000000;
s1 = .;
. += 4;
s2 = .; // 链接脚本中能够直接定义标识符并指定存储位置
.data 0x0804a800:
{
*(.data)
}
.bss :
{
*(.bss)
}
}
3.4 MEMORY命令
默认情况下,链接器认为程序应该加载进入同一个存储空间。
嵌入式系统中,如果存在多个存储空间,必须使用MEMORY进行存储区域定义。
MEMORY命令的使用
MEMORY命令的属性定义
3.5 ENTRY命定指定入口点
链接脚本指定函数入口函数
#include <stdio.h>
#include <stdlib.h>
int program()
{
printf("D.T.Software\n");
exit(0); // 这里如果使用return 0 将产生段错误
}
ENTRY(program)
SECTIONS
{
.text 0x08048400:
{
*(.text)
}
}
gcc -nostartfiles test.c test.lds
链接脚本小结
链接器根据链接脚本中的描述完成具体的工作。
链接脚本用于指定各个段的地址和标识符的地址。
SECTIONS命令确定可执行程序的段信息。
MEMORY命令对存储区进行重定义。
ENTRY命令指定可执行程序的入口函数。
4 链接器、链接脚本、makefile综合示例
4.1 课程实验(模拟嵌入式软件开发)
- 编写一个“体积受限”的可执行程序。
- 通过makefile完成代码编译。
- 运行后在屏幕上打印“D.T.Software”。
4.2 深度分析
4.3 解决方案设计
- 通过内嵌汇编自定义打印函数和退出函数(INT 80H)。
- 通过链接脚本自定义入口函数(不依赖任何库和GCC内置功能)。
- 删除可执行程序中的无用信息(无用段信息、调试信息等)。
打印函数设计
退出函数设计
链接脚本设计
4.4 最后的准备
ld命令:GNU的链接器,将目标文件链接为可执行程序。其是GCC编译器集中的一员,重要的幕后工作者。
ld -static:-static表示ld使用静态链接的方式来产生最终程序,而不是默认的动态链接方式。
gcc -fno-builtin:-fno-builtin参数用于关闭GCC内置函数的功能。GCC提供了很多内置函数(Built-in Function),它会把一些常用的C库函数替换成编译器的内置函数,以达到优化的目的。
4.5 编程实验
program.c
void print(const char* s, int l);
void exit(int code);
void program()
{
print("D.T.Software\n", 13);
exit(0);
}
void print(const char* s, int l)
{
asm volatile (
"movl $4, %%eax\n"
"movl $1, %%ebx\n"
"movl %0, %%ecx\n"
"movl %1, %%edx\n"
"int $0x80 \n"
:
: "r"(s), "r"(l)
: "eax", "ebx", "ecx", "edx"
);
}
void exit(int code)
{
asm volatile (
"movl $1, %%eax\n"
"movl %0, %%ebx\n"
"int $0x80 \n"
:
: "r"(code)
: "eax", "ebx"
);
}
program.lds
ENTRY(program)
SECTIONS
{
.text 0x08048000 + SIZEOF_HEADERS :
{
*(.text)
*(.rodata)
}
/DISCARD/ :
{
*(*)
}
}
makefile
CC := gcc
LD := ld
RM := rm -fr
TARGET := program.out
SRC := $(TARGET:.out=.c)
OBJ := $(TARGET:.out=.o)
LDS := $(TARGET:.out=.lds)
.PHONY : rebuild clean all
$(TARGET) : $(OBJ) $(LDS)
$(LD) -static -T $(LDS) -o $@ $<
@echo "Target File ==> $@"
$(OBJ) : $(SRC)
$(CC) -fno-builtin -o $@ -c $^
rebuild : clean all
all : $(TARGET)
clean :
$(RM) $(TARGET) $(OBJ)
4.6 小结
- 对于资源受限的嵌入式设备,需要考虑可执行程序的大小。
- 通过内嵌汇编直接使用系统服务能够避开相关库的使用。
- 可以通过如下方法控制可执行程序的体积大小。
- 最小化库的使用(必要情况下考虑自己实现相关函数)。
- 自定义链接脚本,删除无用段信息。