一、hello.c 你经历了什么?
话不多说,直接上图:
1. 预处理:hello.c --> hello.i (命令 gcc -E或cpp)
2.编译:hello.i --> hello.s (gcc -0g -S)
3.汇编:hello.s --> hello.o (gcc或as)
4.链接: hello.o + 所需静态库 --> hello(gcc或ld)
其实gcc 是把所有步骤(cpp、cc1、as)都做了一遍。
二、链接,你是什么鬼?
2.1 链接的本质
上图:
没错了,其实链接就是把相同的块(代码、数据)分别合并到一起的过程,当然这只能算是重定位中的一部分吧;所以在合并之前还得进行符号解析,不然咋知道符号引用关系勒。
1.符号解析 Symbol resolution
程序中会有定义和引用的符号(非静态全局函数/变量),局部非静态变量是保存在栈中的,它在链接器眼中就是空气,不存在的。 而编译器会把定义的符号存在一定符号表中,链接器就将符号的引用和确定的符号建立关联。(还有强符号和弱符号的问题,强符合就是函数和初始化的全局变量,弱符号就是未初始化的全局变量)
强弱符号处理规则:
- 不能出现多个同名的强符号,不然就会出现链接错误
- 如果有同名的强符号和弱符号,选择强符号,弱符号是无效的
- 如果有多个弱符号,随便选择一个
那类型不匹配结果会如何呢? 上个例子来看看:
//文件 mismatch-main.c
long int x; /* Weak symbol */
int main(int argc,
char *argv[]) {
printf("%ld\n", x);
return 0;
}
//文件 mismatch-variable.c
/* Global strong symbol */
double x = 3.14;
gcc -Og -o mismatch mismatch-main.c mismatch-variable.c
运行结果:
居然不是默认0,也不是3.14?这么大的数字是从哪来的??什么鬼东西?
这是因为强符合 x=3.14,而浮点数在计算机中的存储、运算、表示等是按IEEE754标准。
再看一个全局变量的例子吧
//文件global.h
extern int g;
int f();
使用extern
关键字 ,其实就是告诉说明这个全局变量,在别的地方也有,所以容易被其他地方的修改所影响到。
//文件global-c1.c
#include "global.h"
int f() {
return g+1;
}
//文件global-c2.c
#include <stdio.h>
#include "global.h"
int g = 0;
int main(int argc, char *argv[]) {
if (argc >= 2) {
g = atoi(argv[1]);
}
printf("g = %d. f() = %d\n", g, f());
return 0;
}
运行: gcc -Wall -Og -o global global-c1.c global-c2.c
结果:
使用的是g=0 强符号,所以没问题。
//文件 global-1.h
#ifdef INITIALIZE
int g = 23;
static int init = 1;
#else
extern int g;
static int init = 0;
#endif
----------------------------
//文件global-c1.c
#include "global-1.h"
int f() {
return g+1;
}
----------------------------
//文件global-c4.c
#define INITIALIZE
#include <stdio.h>
#include "global-1.h"
// int f();
int main(int argc, char** argv) {
if (init){
// do something, e.g., g=31;
//g=31;
int t = f();
printf("Calling f yields %d\n", t);
return 0;
}
}
结果:
说明:注释//g =31时,头文件替换是这样的
而加上g=31后,g被重新赋值了,所以g使用的是重新赋值之后的。
2.重定位 Relocation
重定位其实包括合并.o文件(如上图,把各块合在一起成可执行对象文件),确定每个符号的地址(这是虚拟地址)和在指令中填入新地址这三步。
//文件main.c
int array[2] = {1, 2};
int main(int argc, char** argv)
{
int val = sum(array, 2);
return val;
}
可以算出 sum()函数的地址 0x614=0x60f+0x5。
下图老师的ppt中更易理解,对一些汇编代码进行注解。
已为你打包,随便拿去用吧
比如你想计算某些数学式或排序,或输入输出等,当然这些别人都实现好了,我们直接拿来用就行,比如c语言中的一些库中都有。所以我们用的时候将头文件一包含进来就能使用了,而数学式与输入输出是不一样的,所以它们被打到不同的包中。因为将所有的函数放到一个源文件中,但使用时候只是用到其中之一却要全部加载进来,是很不明智的。
静态库 Static Library
在编译期间由编译器与连接器将它集成至应用程序内,并制作成目标文件以及可以独立运作的可执行文件。具体过程就是把不同文件的 .o 文件通过 Archiver 打包成为一个 .a 文件。
例如:自己编写一个向量运算库 libvector.a
,其中包含两个函数 addvec
和 multvec.
// 文件 main.c
#include <stdio.h>
#include "vector.h"
int x[2] = {1, 2};
int y[2] = {3, 4};
int z[2];
int main()
{
addvec(x, y, z, 2);
printf("z = [%d %d]\n", z[0], z[1]);
return 0;
}
// -----------------------------------------
// 文件 addvec.c
void addvec(int *x, int *y, int *z, int n)
{
int i;
for (i = 0; i < n; i++)
z[i] = x[i] + y[i];
}
// -----------------------------------------
// 文件 multvec.c
void multvec(int *x, int *y, int *z, int n)
{
int i;
for (i = 0; i < n; i++)
z[i] = x[i] * y[i];
}
具体过程是这样的:
gcc -Wall -Og -static -o prog2c main2.o -L. -lvector
gcc -Wall -Og -static -o prog2c main2.o libvector.a
上面两条语句等效。-lvector 就是libvector.a的一种表示 。-Wall是打印出all warning, -O (-O1)是一级优化级别,-g是用于调式。
但如果使用这样顺序链接 gcc -static -o prog2c -L. -lvector main2.o
则会报错:main2.o: In function `main':
main2.c:(.text+0x19): undefined reference to `addvec' collect2: error: ld returned 1 exit status
这是因为链接器是如何解析外部引用,是按顺序查找,所以实际上是有引用依赖问题的。
上面命令中,在编译链接的时候,如果在 main2.o 中发现了外部引用,就会在 -lvector中查找,但是如果反过来,在下面语句中 main2.o 后面没有东西,就会出现找不到引用的错误。
但是我有个疑问就是我在main.c中引用了 sum.c 中的sum()函数,而我直接gcc 编译链接两个文件的时候,无论把main.c和sum.c哪个放在前面都是可以的? 而一步一步执行的时候到链接时候出现了一个不能识别.o文件的错误,查找资料没有得到解决。
共享库 Shared Library
静态库很方便,但是如果我们只是想用库中的某一个函数,却仍然得把所有的内容都链接进去。为了避免了在文件中静态库的大量重复有了共享库。这样所有的程序可以共享同一个库。
1.加载时链接 load-time linking
2.运行时链接 run-time linking
链接过程如下图,运行结果一样。