程序的编译、链接和加载笔记

本文详细介绍了程序从预处理、编译、汇编到链接的全过程,包括预编译处理的步骤、GCC编译指令的使用、链接阶段的地址和空间分配、重定位以及ELF存储分配策略。同时讲解了静态变量的存储位置、自定义段、符号表的内容及其在链接中的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.生成程序目标文件的过程:预处理(Preprocessing),编译(Compilation),汇编(Assembly),链接(Linking)


2. 预编译命令:gcc -E hello.c -o hello.i 或者:cpp hello.c > hello.i

预编译过程主要处理那些源代码文件中的以“#”开始的预编译指令。

1)删除所有的“#define”,并展开所有的宏定义

2)处理所有的条件预编译指令,像“#if”这些

3)处理“#include”预编译指令,将被包含的文件插入到该预编译指令的位置。这个过程是递归进行的

4)删除所有注释

5)添加行号和文件名标识,比如#2 “hello.c” 2,以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号

6)保留所有的#pragma编译器指令,因为编译器要使用它们

GCC调用ccl程序将预处理生成的.i文件编译成以.s为后缀的汇编文件


3. 编译指令:gcc -S hello.i -o hello.s 或者:gcc -S hello.c -o hello.s(合并了预编译和编译两个步骤)

GCC调用as程序将.s文件汇编成机器代码.o的目标文件

编译过程:

当然,中间可能还需要Literal Table、Symbol Tabel、Error Handler等辅助工具


4. 链接:主要过程包括地址和空间分配(Address and Storage Allocation),符号解析(Symbol resolution)和重定位(Relocation)

重定位:比如A.c使用了B.c中定义的全局变量tmp,在编译生成A的目标文件的时候不知道tmp的地址,这样在链接的时候就需要对tmp的地址进行确定(未确定之前一般编译器把地址设为0),地址修正的过程就叫做重定位,每个要被修正的地方就是一个重定位入口。在目标文件中会保留有这些重定位项的信息(.rel.txt, .rel.data和.rel.rodata段)


5. staic int x1 = 0;  static int x2 = 1;

变量x1将会被放在.bss中,而变量x2将被放在.data中。因为x1为0,而未初始化的都是0,所以优化了放在.bss段中可以节省磁盘空间


6. 应用程序可以自定义段,但是自定义的段名使用“.”作前缀,否则容易和系统保留的段名冲突。

可以使用objcopy将图片、音乐等作为目标文件中的一个段:

objcopy -I binary -O elf32-i386 -B i386 image.jpg image.o

$ objdump -ht image.o


image.o: file format elf32-i386

Idx Name          Size      VMA       LMA       File off   Algn
  0 .data         00001308  00000000  00000000  00000034  2**0
                  CONTENTS, ALLOC, LOAD, DATA
SYMBOL TABLE:
00000000 l    d  .data  00000000 .data
00000000 g       .data  00000000 _binary_image_jpg_start
00001308 g       .data  00000000 _binary_image_jpg_end
00001308 g       *ABS*  00000000 _binary_image_jpg_size

_binary_image_jpg_start、_binary_image_jpg_end、_binary_image_jpg_size分别表示该图片文件在内存中的起始地址、结束地址和大小,可以在程序中直接声明和使用


7. 有时候你可能希望变量或某些部分代码能够放到你所指定的段中去,以实现某些特定的功能。比如为了满足某些硬件的内存和I/O的地址布局,或者是像Linux操作系统内核中用来完成一些初始化和用户空间复制时出现页错误异常等。GCC提供了一个扩展机制,使得程序员可以指定变量所处的段:

在全局变量或函数之前加上__attribute__((section("name")))属性就可以把相应的变量或函数放到以name作为段名的段中


8. ELF中的存储分配策略

如果不考虑众多的区段类型,那么链接过程都是一样的。链接器将各个输入文件和库目标文件中的同类型区段收集在一起。链接器还会标出哪些符号会在运行时从共享库中解析,并创建.interp,.got,.plt和符号表区段来支持运行时链接。一旦这些都完成了,链接器会按照传统的顺序来分配空间。与a.out不同,ELF格式不会从0位置加载任何东西,而是从地址空间的蹭部位来加载,这样栈可以在文本段以下向下增长,堆可以在数据段末尾以上向上增长,以更加紧凑地利用地址空间。在386系统上,文本的基地址是0x08048000,这样既能获得位于文本下相当大的栈空间,同时又将0x08000000以上的空间留出来,允许多数程序用来创建单一的二级页表(在386上,每一个二级页表可以映射大小为0x00400000的地址空间)。ELF使用QMAGIC的技巧将头部包括到文本段内,所以实际的文本段起始于ELF头部和程序头部表之后,典型的位于文件偏移量0x100处。然后再将.interp(动态链接器的逻辑连接,需要首先被运行)、动态链接器符号表区段、.init、.text,以及link-once文本和只读数据分配到文本段中。

接下来是数据段,逻辑上起始于文本段末尾的下一个页(因为在运行时方面会同时映射为文本段的最后一页和数据段的第一页)。链接器分配各种.data区段、link-once数据和.got区段,以及一些平台上会用到的.sdata小数据和.got全局偏移量表。

最后是.bss区段,逻辑上紧跟在数据的后面,由.sbss开始(如果有的话,将它放在.sdata和.got的后面),然后是bss段和公共块。


9. 符号表中的符号包括:

1)当前模块中定义(和可能被引用)的全局符号;

2)在模块中被引用但未定义的全局符号(通常称为外部符号);

3)段名称:通常被当作定义在段起始位置的全局符号;

4)非全局符号:调试器或崩溃转储(crash dump)分析通常会用到它们。这些符号几乎不会被链接过程用到,但有时候它们经常会与全局符号混在一起,所以链接器至少要能够跳过它们。在另一些情况中它们会在文件的一个单独的表中,或在一个单独的调试信息文件中;

5)信号信息:告诉源代码语言调试器关于源代码行和目标代码之间的对应关系(可选)。

在链接器内,有一个列出输入文件和库模块的符号表,保留了每一个文件的信息。第二个符号表处理全局符号,即链接器需要在输入文件中进行解析的符号。第三个表可以处理模块内调试符号,尽管少数情况下链接器也会为调试符号建立完整的符号表,但通常都只需将输入的调试符号传递到输出文件

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值