编译器驱动程序和静态链接
先来看一个实例:
main.c
/* main.c */
/* $begin main */
int sum(int *a, int n);
int array[2] = {1, 2};
int main()
{
int val = sum(array, 2);
return val;
}
/* $end main */
sum.c
/* sum.c */
/* $begin sum */
int sum(int *a, int n)
{
int i, s = 0;
for (i = 0; i < n; i++) {
s += a[i];
}
return s;
}
/* $end sum */
代码分析
- 这个示例程序由两个源文件组成,main.c和sum.c。main函数初始化一个整数数组,然后调用sum函数来对数组元素求和。
编译运行
- 想要编译运行此程序,我们需要在shell中输入以下命令来调用GCC驱动程序:
- 运行可执行文件prog,则输入以下命令行:
具体知识点
- 驱动程序要将示例程序从ASCII码源文件翻译成可执行目标文件的过程中,需要经过预处理、编译、汇编、链接四个步骤。
- 从下图中我们可以清楚看到示例程序从ASCII码源文件翻译成可执行目标文件的行为:
- 链接器为了构造可执行文件prog,必须完成两个任务:符号解析和重定位。
可重定位目标文件
-
目标文件有三种形式:可重定位目标文件、可执行目标文件、共享目标文件。
-
典型的ELF可重定位文件格式如下图所示:
-
可执行文件的格式如下:
图源link -
ELF头 描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助连接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(如可重定位、可执行、共享的)、机器类型(如x86-64)、节头部表的文件偏移,以及节头部表中条目的大小和数量。
-
这里只简要介绍部分节的具体内容:
.text:已编译程序的机器代码。
.rodata:只读数据。
.data:已初始化的全局和静态C变量。局部C变量在运行时被保存在栈中,既不出现在.data节中,也不出现在.bss节中。
.bss:未初始化的全局和静态C变量,在目标文件中这个节不占据实际的空间,它仅仅是一个占位符。
.symtab:符号表
符号
- 符号为全局变量和函数,共分为三种:全局符号、外部符号、局部符号。
- 符号会存放在ELF的符号表中,即.symtab节中。用命令行
readelf -s symbols.o
可以看到符号表中的内容,如以下示例:
symbol.c文件
#include <stdio.h>
int time;
int foo(int a) {
int b = a + 1;
return b;
}
int main(int argc, char *argv[])
{
printf("%d\n", foo(5));
return 0;
}
命令行操作结果
- 符号解析:多个目标文件可能会定义相同名字的全局符号,在这种情况下,Linux链接器有一套自己的处理规则:
函数和已初始化的全局变量是强符号,未初始化全局变量是弱符号
- 如果有一个强符号和多个弱符号,选择强符号
- 如果多个弱符号同名,任意选择一个
- 不允许有多个强符号,否则会出现错误
这警示我们在编写程序时不要随意定义全局符号。