深入理解计算机基础第七章

前言

本章主要学习链接器
主要学习分离式编译、c/c++中链接导致的错误、动态库

讲的比较好的PIC实现

gcc的基本知识

菜鸟教程

-ansi 只支持 ANSI 标准的 C 语法。这一选项将禁止 GNU C 的某些特色, 例如 asm 或 typeof 关键词。
-c 只编译并生成目标文件。
-DMACRO 以字符串"1"定义 MACRO 宏。
-DMACRO=DEFN 以字符串"DEFN"定义 MACRO 宏。
-E 只运行 C 预编译器。
-g 生成调试信息。GNU 调试器可利用该信息。
-IDIRECTORY 指定额外的头文件搜索路径DIRECTORY。
-LDIRECTORY 指定额外的函数库搜索路径DIRECTORY。
-lLIBRARY 连接时搜索指定的函数库LIBRARY。
-m486 针对 486 进行代码优化。
-o FILE 生成指定的输出文件。用在生成可执行文件时。
-O0 不进行优化处理。
-O 或 -O1 优化生成代码。
-O2 进一步优化。
-O3 比 -O2 更进一步优化,包括 inline 函数。
-shared 生成共享目标文件。通常用在建立共享库时。
-static 禁止使用共享连接。
-UMACRO 取消对 MACRO 宏的定义。
-w 不生成任何警告信息。
-Wall 生成所有警告信息。

gcc -c main.c func.c 生成main.o func.o
gcc -o demo main.c func.c func2.o 生成demo可执行文件,链接了main.o func.o func2.o
gcc -static -o demo main.o 链接所有.o文件并形成可执行文件

ar rcs libfunc.a func.o func2.o 生成静态库

gcc -shared -fpic -o libfunc.so funca.c funcb.c 生成动态库
gcc -o demo main.c ./libfunc.so 链接动态库(ldd命令可以查看所依赖的动态库)

关于/lib /usr/lib /usr/local/lib

分别是系统级静态库、用户级静态库、个人级静态库
头文件放/usr/local/include
然后放在/usr/local/lib的库文件需要手动加入编译指令,如g++ -odemoe test.cpp -lmy
其中/usr/local/lib/libmy.a,注意-l的时候不需要加lib和后缀
如果连接失败,注意库的依赖关系
此外,命名空间声明和定义分离的时候,二者都要加命名空间修饰,否则输出的二进制token不一样,会导致链接失败

如何生成.o .obj和.a .lib文件

.o不可以重复出现,.a可以重复出现以消除依赖
g++ -static -o demo a.o -L. -lmy -lmy


g++ -c a.cpp
window据说是用lib.exe生成.lib,没找到怎么操作。
MinGW/bin里面有windows下的ar工具,自己下载MinGW或者在devc++的安装目录里有

D:\code\test>g++ -static  -o demo  a.o libmy.a
libmy.a: error adding symbols: Archive has no index; run ranlib to add one
collect2.exe: error: ld returned 1 exit status 

如果出现了这个神秘的错误,把ar工具换一下即可
x86_64-w64-mingw32-gcc-ar rcs = x86_64-w64-mingw32-gcc-ar -r -c -s

x86_64-w64-mingw32-gcc-ar rcs libmy.a b.o c.o

可以nm a.o查看.o文件信息,如果nm: a.o: File format not recognized,则
x86_64-w64-mingw32-gcc-nm a.o

然后g++ -static -o demo a.o libmy.a即可
不加-static会多出一个.obj文件
g++ -static -o demo a.o -L. -lmy这样也可以
-L. = 从当前目录开始搜索
-lmy = libmy.a

链接依赖问题

文件头

windows的文件头称为PE头
linux的文件头称为ELF头

.text 已编译的程序的机器代码
.rodata 只读数据
.data 已初始化的全局和静态C变量
.bss(better save space) 未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。仅仅是个占位符,运行时在内存中分配这些变量,初始化为0
.symtab 一个符号表,存放程序中定义和引用的函数和全局变量的信息,每个可重定位目标都有该表,除非用STRIP指令去除
.rel.text 当链接器把该目标问价和别的可执行文件合并的时候,需要修改该表(一般来说不需要)
.rel.data 被模块引用或者定义的所有全局变量的重定位信息
.debug -g选项调用编译器驱动程序时,才能得到该表
.line 行号到.text指令的映射
.strtab 一个字符串表,包括.symtab和.debug.中的符号表,以及节头部中的节名字(C-style的字符串)
.common 没有初始化的全局变量
.und 没有定义的全局函数
und、common是伪节,仅在重定位文件中,执行文件无

链接器的主要任务

1.符号解析

强符号 : 函数,已经初始化的全局变量
弱符号 : 未初始化的全局变量

约定

1.不允许多个强符号
2.一强多弱选一强
3.多弱任选一个

当弱符号的声明不匹配时,链接器并不会报错

这很容易导致某个头文件中的变量被莫名其妙的修改了...特别是这还可以导致const变量被修改
当你和别人合作时,如果有弱符号相撞...很容易导出非常难查的错误
建议设置链接器,在遇到多重定义的弱符号时,触发错误

2.重定位

符号解析完成后(将每个引用和一个符号定义关联起来),链接器就知道输入的代码节和数据节的确切大小,然后就可以开始重定位

1.步骤一 : 重定位节和符号定义

首先将所有.o文件的相同类型的节合并成一个节
然后链接器将运行时内存地址赋给新的聚合节(中的每个节、符号)
这一步完成时,每一个全局变量和指令都有唯一的运行时地址了

2.步骤二 : 重定位节中的符号引用

修改代码节和数据节中对每个符号的引用,使之指向正确的运行时地址 (这一步依赖于重定位条目)
当汇编器遇到位置未知的引用,就会生成一个重定位条目(重定位条目放在.rel.date和.rel.text中)

重定位条目表
typedef struct
  {
    long offset;      //需要被修改的引用的节偏移
    long type : 32;   //重定位类型
    long symbol : 32; //被修改引用应该指向的符号
    long addend;      //一个有符号常数,一些重定位类型需要它对被修改引用的值做偏移调整
   } Elf64_Rela;
重定位算法

ELF中一共有32种重定位条目类型,我们只关心两种(绝对地址和相对地址
因为内存对齐的原因,磁盘上的符号地址和运行时符号地址有出入
从磁盘文件到装载到内存,节的起始地址会变,但是节内偏移不变
用ADDR(s)代表符号s的运行时地址,假设下面算法运行时,链接器已经为每个节和符号安排了运行时地址
相对地址很简单,重点来看绝对地址

for (s : section)                                 //枚举每个节
  {
  	for (r : relocation_entry in s)           //枚举每个节中的重定位条目
  	  {
  	  	refptr = s + r.offset;            //需要修改的地方
  	  	if (r.type == R_X86_64_PC32)      //相对地址引用,pc + 偏移量
  	  	  {
                        //*refptr = 目标符号的运行时地址 - 引用的运行时地址 + r.addend
  	  	  	refaddr = ADDR(S) + r.offset; //这里求出了运行时的地址
  	  	  	*refptr = ADDR(r.symbol) - refaddr;
                        *refptr += r.addend;
			}
		else if (r.type == R_X86_64_32)  //绝对地址引用
		  {
		  	*refptr = ADDR(r.symbol) + r.addend;
		  }
		}
  }

注意执行call指令的时候,call指令后面跟4字节的地址,而pc 指向四字节地址后面的那条指令
所以相对地址需要加一个恰好为-4的偏移量,也就是四字节的地址
特别注意,PC存的内存和相对地址之间的计算关系

如何加载可执行文件

每个Linux程序都有一个运行时内存映像:

在Linux X86-64系统中,代码总是从地址0x400000开始,后面是数据段,运行时堆在数据段之后,通过调用malloc库往上增长,堆后面的区域是为共享模块保留的。

加载器实际是如何工作的?咕咕咕

静态库

https://www.cnblogs.com/skynet/p/3372855.html
简单来说,就是提供一种打包机制,简化链接。Linux下使用ar工具、Windows下vs使用lib.exe
优点是运行时无需进一步的链接,缺点是比较浪费空间,并且在更新时要重新编译整个文件

链接器如何使用静态库解析引用

比较绕,先来捋捋概念
1.目标文件 : .o文件
2.存档文件 : 也就是静态库
3.可重定位目标文件的集合 E
4.一个未解析的符号集合 U
5.一个在前面输入文件中已定义的符号集合 D
开始时,EDU都为空

注意事项

1.可重定位文件a,b某些部分可能会相互引用,这导致了命令行中的文件名可能需要多次出现消除依赖
2.解析过程和写c/c++的直觉相反,c/c++先写头文件,再写main,链接过程是类似main.o在前,lib_std.o在后面(一般把库.o写到最后面)
3.有依赖关系的库一定要做拓扑排序,并可能多次出现以消除依赖

解析步骤
1 对于命令行上的每个文件 f ,链接器会判断 f 是一个目标文件还是存档文件
1.1 如果是目标文件

链接器将会把这个文件添加到集合E,并根据符号引用情况修改集合U和D的状态。然后处理下一个文件。

1.2 如果是存档文件

链接器将尝试匹配集合U中未解析的符号和存档文件成员定义的符号,如果存档文件的成员m定义了一个符号来解析U中的一个引用,
那么就将m加入到集合E中,然后修改U和D的状态。对存档文件中的每个成员都重复这个过程,直到U和D不再发生变化,然后简单地丢弃不包含在集合E中的成员目标文件。然后链接器继续处理下一个文件。

2 判断集合U是否为空

如果链接器扫描完命令行上的所以文件后,集合U仍不为空,则说明引用了未定义的符号,则链接器将会报错并终止程序。
如果链接器扫描完命令行上的所以文件后,集合U仍为空,则将合并和重定位E中的目标文件,并输出可执行文件。

动态库

特点

1.节省空间(一个动态库,在内存中只有一份拷贝)
2.动态库把对一些库函数的链接载入推迟到程序运行的时期
3.可以实现进程之间的资源共享。(因此动态库也称为共享库)
4.将一些程序升级变得简单
5.甚至可以真正做到链接载入完全由程序员在程序代码中控制(显示调用)
6.动态库在处理类型、虚函数,方面有一些很大的缺陷 : DLL Hell

动态链接共享库

使用动态库生成可执行文件的过程中,静态的执行一部分链接,然后在程序加载时,动态完成剩余部分的链接过程。没有任何的动态库代码和数据节真的被复制到可执行文件中,而是,复制了一些重定位和符号表信息,它们使得运行时可以解析对动态库中代码和数据的引用。

咕咕咕

位置无关代码 PIC

现代系统使用一种方法来编译共享模块的代码段,使得可以把它们加载到内存的任何位置而无需链接器修改。

1.mov指令要求一个绝对地址,怎么转化成相对地址

重要特性

1.数据段和指令段之间的距离是一个常量
2.X86上指令相对偏移的计算

如何把数据绝对地址变为数据相对地址

思路 :
1.段与段之间的距离是固定的
2.通过拿到指令地址计算出指令段的地址
3.计算出数据段的地址
那么就可以计算出数据段的地址(x64上可以直接访问RIP,x86不行),取巧的办法

  call GET_ADDR // 将下一条指令的地址压栈
GET_ADDR : 
  pop ebx       // 弹栈得到ip寄存器的值

用全局偏移表GOT来实现数据位置无关

GOT是一张在date数据段中存储的表,里面记录了很多全局变量的段内绝对地址

假设一条指令想要引用一个变量,并不是直接去用绝对地址,而是去引用GOT里的一个entry。
通过计算GOT表的地址 + entry的偏移,即得到数据的绝对地址
PIC代码具有性能上的缺陷。现在每次全局变量引用都需要5条指令而不是1条,同时GOT还需要占用额外的内存空间。并且,PIC代码需要使用额外的寄存器来保存GOT表项的地址。在寄存器较多的机器上,这不是什么大问题。但是在寄存器较少的IA32系统中,缺少哪怕一个寄存器都可能会触发将寄存器内容暂存在堆栈里。

2.怎么延迟绑定函数地址

延迟绑定需要在两个数据结构之间进行密集而复杂的交互
GOT和过程连接表(procedure linkage table, PLT)。
如果一个目标模块调用了共享库中的任意函数,那么它就有它自己的GOT和PLT。
GOT是.data段的一部分。PLT是.text段的一部分。

库打桩机制

和windows下的hook类似,可以拦截动态库中的调用
下面是它的基本思路:给定一个需要打桩的目标函数,创建一个包装函数,它的原型与目标函数完全一样。使用某种特殊的打桩机制,你就可以欺骗系统调用包装函数而不是目标函数了。包装函数通常会执行它自己的逻辑,然后调用目标函数,再将目标函数的返回值传递给调用者。
打桩可以发生在编译时、链接时或当程序被加载和执行的运行时。
库打桩最重要的就是实现mymalloca能调用malloca这样,自引用

1.编译时打桩(需要访问源代码)

// int.c
#include <stdio.h>
#include <malloc.h>
int main()
  {
  	int *p = malloc(32);
  	free(p);
  	return 0;
   } 

malloc.h

// malloc.h
#define malloc(size) mymalloc(size)
#define free(ptr) myfree(ptr)
void* mymalloc(size_t size);
void myfree(void *ptr);

mymalloc.c

// mymalloc.c
#ifdef COMPILETIME
#include <stdio.h>
#include <malloc.h>
void* mymalloc(size_t size)
  {
  	void *ptr = malloc(size);
  	printf("malloc(%d) = %p\n",(int)size,ptr);
  	return ptr;
  }
void myfree(void *ptr)
  {
  	free(ptr);
  	printf("free(%p)\n",ptr);
  }
#endif 

-I.:指示C预处理器在搜索通常的系统目录前,先在当前目录中查找malloc.h

gcc -DCOMPILETIME -c mymalloc.c  //-DCOMPILETIME等效于#define DCOMPILETIME
gcc -I . -o intc int.c mymalloc.o

待实操

2.链接时打桩(需要访问可重定位对象文件)

// mymalloc.c
#ifdef LINKTIME
#include <stdio.h>
void *__real_malloc(size_t size);
void  __real_free(void *ptr);
void *__wrap_malloc(size_t size)
  {
  	void* ptr = __real_malloc(size); // 调用libc::malloc 
  	printf("malloc(%d) = %p\n",int(size),ptr);
  	return ptr;
  }
void __wrap_free(void *ptr)
  {
  	__real_free(ptr); // 调用 libc::free
	printf("free(%p)\n",ptr);
  }
#enif
gcc -DLINKTIME -c mymalloc.c
gcc -c int.c
gcc -W1,--wrap,malloc -W1,--wrap,free -o int1 int.o mymalloc.o

-W1,option标志把option传递给链接器,option中的每个逗号都要替换为一个空格,所以-w1,--wrap,malloc
就把--wrap malloc传递给链接器

3.运行时打桩(需要访问可执行文件)

这个很厉害的机制基于动态链接器的LD_PRELOAD环境变量
正常写一个用了malloc或者new的代码,并正常编译为./intr
然后用

g++ -std=c++11 -DRUNTIME -shared -fpic -o mymalloc.so mymalloc.cpp -ldl

编译出一个动态库

#ifdef RUNTIME
//#define _GNU_SOURCE
#include <ctime>
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <iostream>
#include <fstream>
#include <string>
std::string GetTime(){
    using namespace std;
    time_t now = time(0);
    tm *ltm = localtime(&now);
    long long int tmp = 0;
    tmp += 1900 + ltm->tm_year;
    tmp *= 100;
    tmp += 1 + ltm->tm_mon;
    tmp *= 100;
    tmp += ltm->tm_mday;
    tmp *= 100;
    tmp += ltm->tm_hour;
    tmp *= 100;
    tmp += ltm->tm_min;
    tmp *= 100;
    tmp += ltm->tm_sec;
    string ret = to_string(tmp);
    ret.insert(12,":");
    ret.insert(10,":");
    ret.insert(8,"-");
    return ret; 
}
const char * const output = "/home/mzb/Project/MemoryLeak/Memory.log";
FILE* fout = NULL;
using std::endl;
using std::cout;
using std::cerr;
static int is_self_reference = 0;

void init(){
    static bool is_init = false;
    if (!is_init){
        is_init = true;
        ++is_self_reference;
        fout = fopen(output,"at+");
        --is_self_reference;
    }
}

void* Dlsym(const char* const func_name){
    auto ret = dlsym(RTLD_NEXT,func_name);
    auto error = dlerror();
    if (error != NULL){
        cerr << "dlsym(" << func_name << ") = " << ret << endl;
        cerr << "dlerror() = " << error << endl;
        exit(0);
    }
    else{
        return ret;
    }
}
void* malloc(size_t size){
    init();
    using func_type = void* (*)(size_t size);
    func_type mallocp = (func_type)Dlsym("malloc");

    if (is_self_reference){
        return mallocp(size);
    }
    else{
        ++is_self_reference;
        auto ret = mallocp(size);
        if (fout){
            auto time = GetTime();
            fprintf(fout,"%s malloc(%d) = %p\n",time.c_str(),(int)size,ret);
        }
        else{
            cerr << "fopen(" << output << ",at+) failed" << endl;
        }
        --is_self_reference;
        return ret;
   }
}

void free(void* ptr){
    using func_type =  void (*)(void *);
    func_type freep = (func_type)Dlsym("free");
    if (is_self_reference){
        freep(ptr);
    }
    else{
        ++is_self_reference;
        if (fout){
            auto time = GetTime();
            fprintf(fout,"%s free(%p)\n",time.c_str(),ptr);
        }    
        else{
            cerr << "fopen(" << output << ",at+) failed" << endl;
        }
        freep(ptr);
        --is_self_reference;
    }
}
#endif

最后,使用bash LD_PRELOAD="./mymalloc.so" ./intr 运行程序即可,记录结果在Memory.log里面

posted @ 2021-07-15 14:30  XDU18清欢  阅读(308)  评论(0)    收藏  举报