[操作系统]链接

虽然编译原理还没有开始学习,但是libco中用到的hook技术已然用到了其中的一些知识.于是发奋拿起了<<深入理解计算机系统>>看了一下链接的篇章.在此处做一个简单的总结!!!

链接可以执行与编译时,也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到存储器并执行时;甚至可以执行于运行时,由应用程序来执行。

从传统静态链接到加载时的共享库的动态链接,以及到运行时的共享库的动态链接。

[操作系统]链接

编译驱动程序(其实就是那一套流程)

用到的两个程序代码:
swap.c

/* $begin swap */
/* swap.c */
extern int buf[];
 
int *bufp0 = &buf[0];
int *bufp1;
 
void swap() 
{
    int temp;
  
    bufp1 = &buf[1];
    temp = *bufp0;
    *bufp0 = *bufp1;
    *bufp1 = temp;
}
/* $end swap */
 

main.c

/* $begin main */
/* main.c */
void swap();
 
int buf[2] = {1, 2};
 
int main() 
{
    swap();
    return 0;
}
/* $end main */
 

[操作系统]链接
[操作系统]链接
由预处理器(cpp)将main.c翻译成中间文件:main.i,接下来是编译器(cc1)将main.i翻译成汇编文件main.s。然后是汇编器(as)将main.s翻译成一个可重定位的目标文件main.o。最后由链接器(ld)将main.o和swap.o以及一些系统目标文件组合起来,创建可执行目标文件p

静态链接

以一组可重定位目标文件和命令行(这个其实就是前面的编译器和汇编器传过来的引导连接器和加载器的数据结构)为输入,以一个可执行文件为输出.

  • 可重定位目标文件:由各种不同的代码和数据节组成每一节都是一个连续的字节序列,指令在一节,初始化的全局变量在一节,未初始化的变量在另外一节.
    [操作系统]链接

链接器必须完成两个主要任务:

  • ① 符号解析。目标文件定义和引用符号,(也就是说每一个符号都会对应一个函数,变量等..).符号解析的目的是将每个符号引用和一个符号定义联系起来;

  • ②重定位:编译器和汇编器生成从地址0开始的代码和数据节,连接器通过把每个符号与一个内存位置对应起来,然后修改对这些符号的引用,使得他们指向这个存储器位置,从而实现重定位。

链接器操作的目标文件究竟是什么?

目标文件一般是由汇编器生成的.o后缀的文件,大概有三种不同的形式:

  • 可重定位目标文件;
  • 可执行目标文件
  • 共享目标文件(一种特殊的可重定位目标文件,在加载或者运行时被动的加载进内存被链接)

我们接下来讨论的目标文件是基于Unix系统的ELF格式(Exxcutable and Linkable Format),这同Windows系统上的PE(Portable Executable)文件格式在基本概念上其实是相似的:

  • 一个典型的ELF可重定位目标文件的格式:
    [操作系统]链接

解释:
.text:已编译程序的机器码;.rodata:只读数据(read-only-data);
.data:已初始化的全局C变量;.bss:未初始化的全局C变量(better save space);
.symtab:一个符号表(定义和引用的函数和全局变量信息);
.rel.text:代码重定位条目, 一个.text节中位置的列表,需要修改的位置;
.rel.data: 被模块引用或定义的任何全局变量的重定位信息;
.debug:一个调试符号表; .line:原始C源程序中的行号和.text机器指令的映射;
.strtab: 一个字符串表

符号和符号表(由编译器构造,每一个可重定位目标文件都有一张符号表,作用就是表示每一个符号对应的信息(比如:函数?外部符号?全局符号?))

保存于.symtab中的是一个符号表,其是定义和引用函数和全局变量的信息。有三种不同类型的符号:

  • 全局符号(不带static的C函数以及全局变量)
  • 外部引用(外部定义的不带static的C函数以及全局变量)
  • 本地符号(带 static的C函数以及全局变量)

注意:本地程序变量在符号表中不出现,直接放在栈中管理

如果是带有static符号的就会在.data 和.bss中为每个定义分配空间,并在.symtab中创建一个唯一名字的本地符号。比如:
[操作系统]链接
中有两个static定义的x变量,其会在.data中分配空间,并在.symtab中创建两个符号,x.1表示f函数的定义和x.2表示函数g的定义。(注:使用static可以保护你自己的变量和函数)

符号表的结构:

[操作系统]链接
我们给出main.o符号表中的最后三个条目:(开始的都是使用的本地符号)

[操作系统]链接
我们看到num8处,的全局变量buf定义条目,位于.data(Ndx=3)开始字节偏移为0(value为0)处的8个字节目标(size)。随后是全局符号main的定义,其位于.text(Nex=1)处,偏移字节为0处(value)的17个字节函数。最后一个是swap的引用,所以是Und。(这个应该有点问题,去看一下书中的解释吧!Ndx== 1 .text 节,Ndx == 3, .data 节)

链接器终于开始工作了

1 符号解析(开始链接器的第一个任务)

符号解析任务简单的说,就是**链接器在这一个阶段的主要任务就是把代码中的每个符号引用和输入的可重定位文件的符号表中确定的一个符号定义联系起来.对于本地符号,这个任务相对来说是简单的。复杂的就是全局符号,编译器(cc1)遇到不是在当前模块中定义的符号时,会假设该符号的定义在其他模块中,生成一个链接器符号交给链接器处理。如果链接器ld在所有的模块中都找不到定义的话就会抛出异常。**

这里最容易产生的错误就是当多个模块定义同一个符号的时候,我们的链接器到底怎么做。以C++中的函数重载为例,我们会按照实际的需要重载许多相同名字的函数,链接器(ld)使用一种叫做毁坏的方法(mangling)将相同函数名不同参数的函数,比如Foo将会编码成3Foo__的形式,实际上还是使得在链接器层面上来看符号是唯一的。

  • 链接器如何解析多重定义的全局符号:

在编译时,编译器向汇编器输出每个全局符号,要么是强,要么是弱.汇编器吧这个放到符号表中.强弱定义:

  • 强:函数+已初始化的全局变量
  • 弱:未初始化的全局变量

使用如下规则:

规则1:不允许多个强符号;

规则2:如果有一个强符号和多个弱符号,那么选择强符号;

规则3:如果有多个弱符号,那么这些弱符号中任意选择一个;

具体解析会有三个集合,然后balabalabalabala的,太细节了,应该不用明白吧!!具体看书

2 重定位

合并输入模块,并为每个符号分配运行时的地址一般有两步:

  • 重定位节和符号定义:在这一步中,链接器将所有模块中的.data节合并成一个文件的.data节,运行时存储器的地址也会赋给新的聚合节,完成时,每条指令和全局变量都有唯一的运行时内存地址了
  • 重定位节和符号引用:修改代码和数据节中对于每个符号的引用,使得他们指向正确的运行时地址

可执行目标文件格式(一个典型的ELF可执行文件)

[操作系统]链接
说明:

  • ELF头部:描述文件总体格式,标注出程序入口点;.init:定义了初始化函数;

  • 段头部表:可执行文件是一个连续的片,段头部表中描述了这种映射关系;

如何加载可执行目标文件

[操作系统]链接

  • 加载后运行的每个Unix程序都有一个镜像,如上图所示。代码段总是从0x08048000开始,数据段是接下来的4kb对齐地址处,运行时堆在读写段之后,使用malloc向上增长;还有一个段为共享库保留。用户栈是在最大合法地址处开始并向下增长。再往上就是不对用户开放的内核虚拟存储器了。

  • 什么是加载?说白了就是将程序拷贝到存储器并运行的过程。这里是由execev函数来调用加载器(驻留在存储器中)完成的,我们要执行p文件的时候,就是使用./p来,加载器就把p的数据和代码拷贝从磁盘拷贝到了存储器中,并通过跳转到ELF头部中的程序入口点开始程序p的执行。

  • 怎样加载?当加载器运行时,就先创建一个存储器映像(上图所示),在ELF可执行文件头部表的指示下,加载器将可执行文件的代码和数据段拷贝到0x0804800处向上的两个段中,然后跳转到程序入口点_start(在ctrl.o中定义)开始执行

库打桩机制

Linux链接器所提供的技术,允许用户截获对共享库函数的调用,并执行自己的代码(当然是在普通权限下,管理员权限通常是禁止使用该技术的)。
使用打桩机制,可以追踪某个特殊库函数的调用次数、验证并追踪其输入输出,甚至把它替换成一个完全不同的实现。

三种方式:

  • 编译时打桩(宏定义)
    //主要是这,具体看书
#ifndef COMPILE_TIME
#define malloc(size) mymalloc(size) 
#define free(ptr) myfree(ptr)
  • 链接时打桩(相当于简单的替换)

Linux静态链接器支持用–wrap f标志进行链接时打桩。这个标志告诉链接器,把对符号f的引用解析成__wrap_f(前缀是两个下划线),还要对符号__real_f的引用解析成f。

  • 运行时打桩

编译时打桩需要访问程序的源代码,连接时打桩需要能够访问程序的可重定位的对象文件。不过运行时打桩仅需要访问可执行目标文件即可,它的基本原理是基于动态链接器的LD_PRELOAD环境变量的。

如果LD_PRELOAD环境变量被设置为一个共享库路径的列表(以空格或分号分隔),那么当你加载和执行一个程序,需要解析未定义的引用时,动态链接器会先搜做LD_PRELOAD中给定的库,然后才搜索任何其他的库。有了这个机制,当你加载和执行任意可执行文件时,可以对任何共享库中任意函数打桩,包括libc.so中的malloc和free。
// mymalloc.c

#ifdef RUNTIME
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>

// malloc wrapper function
void * malloc(size_t size) {
    printf("%s enter %u\n", __FUNCTION__, size);
    void *(* mallocp)(size_t size);
    char * error;
    
    // get address of libc malloc
    mallocp = dlsym(RTLD_NEXT, "malloc"); //dlsym 从libc 库获得系统调用
    if ((error = dlerror()) != NULL) {
        fputs(error, stderr);
        exit(1);
    }
    void * ptr = mallocp(size);
    printf("malloc %p size %u\n", ptr, (int)size);
    return ptr;
}

// free wrapper function
void free(void *ptr) {
    void (* freep)(void *ptr);
    char * error;
    
    // get address of libc free
    freep = dlsym(RTLD_NEXT, "free");
    if ((error = dlerror()) != NULL) {
        fputs(error, stderr);
        exit(1);
    }

    freep(ptr);
    printf("free %p\n", ptr);
}
#endif

参考:
https://www.jianshu.com/p/7f27c0316355