连接学习与工作
背景
现在商用软件规模以几十万行代码计,因此我们会多人协作,分别开发不同的模块。在后续维护过程中也会针对模块查找问题,不至于迷失在大量的代码当中。静态链接就是一个简单的解耦手段。这一篇我们只围绕静态链接讨论,动态链接放在后续讨论。
原理
每个人负责一个小块,同时把这一小块的对外接口提供出来,最终组装成一个完整拼图。这个组装的过程,就是静态链接的过程。
声明的作用
add.h
#ifndef __ADD_H
#define __ADD_H
int add(int a, int b);
#endif
main.c
#include <stdio.h>
int main(int argc, char* argv[])
{
int a = 1, b = 2;
int result = add(a, b);
printf("%d", result);
return 0;
}
g++ -c main.c add.h
main.c: In function ‘int main(int, char**)’:
main.c:6:23: error: ‘add’ was not declared in this scope
如果没有在使用add函数之前声明int add(int a, int b)就会报找不到add这个符号。
接下来,在main.c当中加上#include “add.h”,在预处理阶段,头文件展开,就会把int add(int a, int b)的声明放到main.c当中。
#include <stdio.h>
#include "add.h"
int main(int argc, char* argv[])
{
int a = 1, b = 2;
int result = add(a, b);
printf("%d", result);
return 0;
}
g++ -c main.c add.h
编译成功,生成main.o(目标文件,非可执行文件,因为还没有链接)
Q: 为什么我的add函数根本没有实现,它还会编译通过呢?
A: 编译过程中我们的输入时main.c和add.h,输出是main.o。声明的作用就是add.h告诉编译器,我提供了一个int add(int a, int b)的函数给其他人使用。编译器知道了之后,在编译main.c过程中发现add没有在main.c定义,就会把add标记为一个外部符号,放到自己的重定位表当中(重定位表在main.o里面存着),而不会报错。
objdump -r main.o
main.o: file format elf64-x86-64RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
0000000000000028 R_X86_64_PC32 _Z3addii0x0000000000000004
0000000000000035 R_X86_64_32 .rodata
000000000000003f R_X86_64_PC32 printf0x0000000000000004RELOCATION RECORDS FOR [.eh_frame]:
OFFSET TYPE VALUE
0000000000000020 R_X86_64_PC32 .text
重定位表中包含有add和printf两个函数的记录。
readelf -s main.o
_Z3addii,printf的Ndx为undefine,说明这两个符号在main.o中为未定义的。
定义的作用
g++ main.o add.h -o main
main.o: In function ‘main’:
main.c:(.text+0x28): undefined reference to ‘add(int, int)’
collect2: error: ld returned 1 exit status
从上图中,我们知道main.o里面有两个函数_Z3addii,printf为undefine的。这里报的错误是未定义的add函数。printf在#include
#include "add.h"
int add(int a, int b)
{
return a + b;
}
g++ -c add.c
g++ main.o add.o -o main
Q:重定位是怎么做的呢?
A:add.o当中add这个函数在代码段中的相对位置已经确定(设为addressA),现在把add.o和main.o合并成了main(可执行文件),那么add的代码段在main当中的位置就发生了偏移(设偏移地址为x),但是add函数相对于add的代码段的位置没有变化,因此add函数的地址就变成了(x+addressA)。
main.o中使用了add函数,使用的位置(比如是address1,address2)在main.o的重定位表当中存储着,那么随着main.o合并入main,其代码段偏移量为(x),那么使用add函数的位置就变成了(x+address1,x+address2),此时编译器会把这两个位置的add函数的地址替换成x+addressA。这样main在执行过程中就能正确找到add函数的位置了。
编译选项
头文件搜索路径
把add.h搬到上一级目录(不要是系统默认的include path就行)
g++ -c main.c
main.c:2:17: fatal error: add.h: No such file or directory
找不到add.h
g++ -c main.c -I../
-I用于指定除默认头文件搜索路径之外的头文件搜索路径。
静态链接库
请看:http://blog.youkuaiyun.com/xuhongning/article/details/6365200
ar rcs libadd.a add.o
g++ main.o libadd.a -o main
编译完成之后,即使把libadd.a删除,也不会影响main执行。因为libadd.a会被合成到main当中。因此在实际应用中,静态库中的.o通常是只有几个函数的小文件的集合,而非全部函数的一个大文件,这样可以保证生成的可执行文件比较小。
总结
想深入了解,则需要结合《程序员的自我修养》这本书,再在实战过程中需要大量的反编译才能真正了解每一个字节。但我认为没有必要,因为实际情况是,我会用别人提供的库,会给别人提供正确的库,就已足够,没必要纠结ELF哪个字段是哪个含义,太学究了。
本文主要说明了在静态链接中,声明的作用,定义的作用,以及由于误用导致的错误,为实际工作中遇到编译链接失败提供一个指向型的分析方法。
个人理解,欢迎拍砖
目标文件也好,可执行文件也好,都是我们编写的代码的另一种表现形式。因此它们不会涉及到堆栈的分配等问题。比如代码中写了int *a = new int[10]。那它会在代码段中把这个new的指令保存下来,而不会在可执行文件中开辟10个整形空间。因为ELF,.o也没有堆栈这个东西。堆栈只有在运行时,执行指令的时候,才会去运用。