"); //-->
有助于构造大型程序
有助于避免一些危险编程错误
有助于理解其他重要的系统概念
让你能够利用共享库
$ gcc -O2 -g -o p main.c swap.c
1. 运行C预处理器(cpp),将main.c翻译成一个中间文件 $cpp [options] main.c main.i 2. 运行C编译器(ccl),将main.i翻译成汇编语言 $ccl main.i main.c -O2 [options] -o main.s $gcc -S main.c -O2 [options] -o main.s 3. 运行汇编器(as),将main.s翻译成可重定位目标文件(relocatable object file) main.o $as [options] -o main.o main.s 4. 重复以上步骤生成swap.o 5. 运行连接器(ld),将main.o swap.o以及一些必要的系统目标文件组合起来,生成可执行目标文件(executable object file) p $ ld -o p [system object files and args] main.o swap.o
$ ./p
1. 符号解析(symbol resolution) 将符号引用(object reference)和符号定义联系起来 2. 重定位(relocation) 编译器和汇编器生成从地址0开始的代码和数据节(section),链接器通过把每个符号定义与一个内存地址联系起来,然后修改所有对这些符号的引用, 使得他们指向这个内存地址,从而重定位这些sections
可重定位目标文件
可执行目标文件
共享目标文件:可动态加载到存储器与可执行文件链接执行
COFF(Common Object File Format):System V Unix早期版本使用
PE(Portable Executable):COFF变种,Windows NT使用
ELF(Executable and Linkable Format):System V Unix后来的版本使用
.text: 已编译程序的机器代码 .rodata: 只读数据 .data: 已初始化的全局变量(ELF文件中不含局部变量,他们保存在栈中) .bss:(Block Storage Start) 未初始化的全局变量,区分已初始化和未初始化全局变量的目的是为了节省磁盘空间,目标文件中这个节不占用空间,只是一个占位符 .symtab: 符号表,存放程序中定义和引用的函数和全局变量的信息(没有局部变量的条目) .rel.text: 一个.text节中位置的列表。当链接器将此文件与其他目标文件链接时需要修改这些位置,一般任何调用外部函数或引用全局变量的指令都要修改 .rel.data: 引用或定义的任何全局变量的重定位信息,任何已初始化的全局变量,如果它的初值是一个全局变量地址或外部函数地址,就需要修改 .debug: 调试符号表,包含了程序中定义的局部变量和类型定义,定义或引用的全局变量,以及源文件。编译时使用-g选项才能生成这个section .line: 源文件中的行号和.text节中机器指令间的映射,编译时使用-g生成这个表 .strtab: 字符串表,包含.symtab和.debug节中的符号表,以及节头部中的节名字
1. 由m定义,能被其他模块引用的全局符号。非static函数和非static全局变量 2. 其他模块定义,被m引用的全局符号。 源文件中使用external修饰 3. 只被m定义和引用的本地符号。带static的函数和带static的全局变量和本地变量
利用static隐藏变量和函数名: 一个源文件中声明的全局变量和函数,其他模块都可以看到。如果不想其他模块使用全局变量和函数,可以用static修饰,static全局变量和函数只有声明它的源文件可用
name: symbol名字,指向字符串表中的字节偏移量 value: 符号地址 size: 目标大小 type/binding: 目标类型,binding表示符号是本地还是全局的 reserved: 保留 section: 每个符号都和目标文件中某个节相关联,这个字段存储的是到节头部表的索引。除了具体节,还有3个伪节(pseudo section): ABS:不该被重定位的符号 UNDEF:未定义符号,表明被这个目标文件引用,但是在其他地方定义 COMMON:表示还未分配位置的未初始化的数据目标
规则1:不允许有多个同名强符号
规则2:如果有一个强符号和多个同名弱符号,那么选择强符号
规则3:如果有多个同名弱符号,那么从中任选一个
$ ar rcs libvector.a addvec.o multvec.o
可重定位目标文件集合E
未解析的符号集合U
在输入文件中已定义的符号集合D
对于输入文件f,判断它是一个目标文件还是一个库文件
如果是目标文件,添加到E,并且扫描f里的符号定义和引用来修改集合U和D。继续下一个文件
如果f是库文件,尝试在库文件中查找U中未定义的符号。如果在库文件的某个成员m中找到一个符号来解析U中的引用,就将m添加到E,并且扫描m来修改U和D。对库文件中所有成员目标文件都反复进行此过程,直到U和D都不再变化
当处理完所有文件,如果U是非空,那么就会产生链接错误。否则就就合并和重定位E中的目标文件,构建可执行文件
重定位节和符号定义:链接器将所有相同类型的节合并到同一类型的新的聚合节,并将此节作为可执行文件的对应节。随后链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及每个符号,现在程序中每个指令和全局变量都有唯一的运行时地址了
重定位节中的符号引用:链接器修改代码和数据节中的符号引用,让他们指向正确的运行时地址。这一步需要“重定位条目”的支持
[cpp] view plaincopytypedef struct { [cpp] view plaincopyint offset; /* Offset of the reference to relocate */ [cpp] view plaincopyint symbol:24, /* Symbol of the reference should point to */ [cpp] view plaincopytype:8; /* Relocation type */ [cpp] view plaincopy} Elf32_Rel;
R_386_PC32:重定位一个使用32位PC相对地址的引用
R_386_32:重定位一个使用32位绝对地址的引用
foreach section s { foreach relocation entry r { refptr = s + r.offset; /* ptr to reference to be relocated */ /* Relocate a PC-relative reference */ if (r.type == R_386_PC32) { refaddr = ADDR(s) + r.offset; /* ref's runtime address */ *refptr = (unsigned) (ADDR(r.symbol) + *refptr - refaddr); } /* Relocate an obsolute reference */ if (r.type == R_386_32) *refptr = (unsigned) (ADDR(r.symbol) + *refptr); } }
$ objdump -d main.o .... 6: e8 fc ff ff ff call 7 <main+0x7> swap(); 7: R_386_PC32 swap relocation entry .....
r.offset = 0x7
r.symbol = swap
r.type = R_386_PC32
ADDR(s) = ADDR(.text) = 0x80483b4
ADDR(r.symbol) = ADDR(swap) = 0x80483c8
refaddr = ADDR(s) + r.offset
= 0x80483b4 + 0x7
= 0x80483bb
*refptr = (unsigned) (ADDR(r.symbol) + *refptr - refaddr)
= (unsigned) (0x80483c8 + (-4) - 0x80483bb)
= (unsigned) (0x9)
80483ba: e8 09 00 00 00 call 80483c8 <swap> swap();
push PC onto stack
PC <- PC + 0x9 = 0x80483bf + 0x9 = 0x80483c8
注意:
为什么call指令中引用的初始值为-4?
这是因为CPU执行call指令时,PC实际指向了下一条指令,然而引用的开始地址是下一条指令之前的4 bytes处(因为引用占4 bytes)。
int *bufp0 = &buf[0];
00000000 <bufp0>:
0: 00 00 00 00 int *bufp0 = &buf[0];
0: R_386_32 buf Relocation entry
ADDR(r.symbol) = ADDR(buf) = 0x8049454
*refptr = (unsigned) (ADDR(r.symbol) + *refptr)
= (unsigned) (0x8049454 + 0)
= (unsigned) (0x8049454)
0804945c <bufp0>:
804945c: 54 94 04 08 Relocated
$ gcc -shared -fPIC -o libvector.so addvec.c multvec.c
$ gcc -o p2 main.c libvector.so
重定位libc.so的文本和数据到某个内存段
重定位libvector.so的文本和数据到另一个内存段
重定位可执行文件中所有对libc.so libvector.so中符号的引用
$ gcc -rdynamic -O2 -o p3 dll.c -ldl
#include <dlfcn.h>
void* dlopen(const char* filename, int flag); // 成功时返回指针为指向句柄的指针,否则返回NULL
flag:
RTLD_GLOBAL: 解析库‘filename’中的外部符号
RTLD_NOW: 让链接器现在就解析符号引用
RTLD_LAZY: 使用到该符号引用时才解析
#include <dlfcn.h>
void dlsym(void *handle, char *symbol); // 成功则返回指向符号的指针,否则返回NULL
#include <dlfcn.h>
int dlclose(void* handle); // 成功返回0, 否则返回-1
#include <dlfcn.h>
const char* dlerror(void); //如果dlopen、dlsym、dlclose调用失败,则返回错误信息,成功则返回NULL
call L1 L1: popl %ebx ebx contains the current PC addl $VAROFF, %ebx ebx points to the GOT entry for the var movl (%ebx), %eax reference indirect through the GOT movl (%eax), %eax got the real content of the reference 为什么popl %ebx会得到PC的值? 这是因为call L1会将当前PC的值压栈后再跳转到L1处开始执行,所以popl指令取的其实就是call压入的PC值。 取PC值的目的是什么呢? 当然是为了找到GOT中当前引用对应的条目,因为引用实际上存的是它在GOT中对应条目相对于下一条指令地址(PC值)的偏移量, 所以(%PC)加上这个偏移量就是此引用在GOT中对应的条目。
call L1
popl %ebx ebx contains the current PC
addl $PROCOFF, %ebx ebx points to the GOT entry for proc
call *(%ebx) call indirect through the GOT
08485bb: e8 a4 fe ff ff call 8048464 <addvec>
call指令使用相对寻址方式,实际地址为当前PC地址+0xfffffea4 = 0x8048464, 刚好就是PLT[2]开始的地址
AR:创建静态库,插入、删除、列出和提取成员
STRINGS:列出一个目标文件中所有可打印的字符串
STRIP:从目标文件中删除符号表信息
NM:列出一个目标文件的符号表中定义的符号
SIZE:列出目标文件中节的名字和大小
READELF:显示一个目标文件的完整结构,包括ELF头中编码的所有信息。包含SIZE和NM的功能
OBJDUMP:所有二进制工具之母。。能够显示一个目标文件的所有信息。最大作用就是反汇编.text中的二进制指令
LDD:列出可执行文件在运行时需要的共享库
https://blog.csdn.net/u012409883/article/details/49559695
*博客内容为网友个人发布,仅代表博主个人观点,如有侵权请联系工作人员删除。