深入理解计算机系统第七章知识点总结

详解ELF文件-> main.o

  • elf文件可以分成三部分

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sfedvODD-1684661656490)(E:\csdn_blog博客\大三\上\计算机系统二\image\elf.png)]

    • ELF头——ELF header
    • 不同的section——Sections
    • 以及描述这些section信息的表——Section header table

假设有如下main.c文件

#include<stdio.h>

int count = 10;
int value;

void func(int sum)
{
	printf("sum is:%d\n", sum);
}

int main()
{
	static int a = 1;
	static int b = 0;
	int x = 1;
	func(a + b + x);
	return 0;
}

  • ELF头

    • gcc -c编译后产生main.o文件然后使用readelf -h main.o查看ELF

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4nPeons2-1684661656491)(E:\csdn_blog博客\大三\上\计算机系统二\image\elf头.png)]

前十六个字节的含义

  • 7f 45 4c 46前四个字节被称为ELF文件的魔数

    • 分别与ascll码中的DEL控制符、字符E、字符L、字符F对应
    • 魔数就是用来确认文件类型的
  • 02 01 01

    • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HJRap49R-1684661656492)(E:\csdn_blog博客\大三\上\计算机系统二\image\magic.png)]
  • 最后9个字节ELF的标准中没有定义,用0填充

推测elf的大小

  • 本头的大小可以推出sections的起始地址,这里就是0x40

  • 推测整个elf文件的大小, 利用本头的大小和下图的三个信息

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lDpO3Lks-1684661656492)(E:\csdn_blog博客\大三\上\计算机系统二\image\elf文件大小推断.png)]

    • start of section headers:可以确定节头部表的起始位置

    • 节头大小节头数量可以确定节头表的大小

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xjhxQGwj-1684661656492)(E:\csdn_blog博客\大三\上\计算机系统二\image\1064.png)]

查看节头部表

readelf -S main.o:查看节头部表的信息

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IxFitNHp-1684661656492)(E:\csdn_blog博客\大三\上\计算机系统二\image\节头部表.png)]

推断每个sectionelf中的具体位置

  • 整个elf一共包含12section

  • offset代表每个section的起始位置

  • size每个section的大小

    • 例如.text这一section他的offset0x40,大小为0x54,由于elf头的大小为0x40,所以.text是紧跟在elf的后面的

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TYzrzzH5-1684661656493)(E:\csdn_blog博客\大三\上\计算机系统二\image\text.png)]

查看.text的内容

已知.text是存放已编译程序的机器代码

objdump -s -d main.o

  • -s:查看机器指令
  • -d:查看汇编代码

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HdVo2kcp-1684661656493)(E:\csdn_blog博客\大三\上\计算机系统二\复习\image\text机器代码.png)]

  • 前两个字节是指令地址
  • 后面的指令是机器指令
  • 每个指令对应的汇编代码可以在-d中查看

查看.data的内容

已知.data是存放已初始化的全局和静态C变量,而初始化为0的全局和静态变量被存在.bss

objdump -s -d main.o

  • 由于.text的起始位置是0x40,大小为0x56,故可以推断出下一个section的起始位置是0x94,查表可知.dataoffset0x94,刚好对应的上

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qukAji68-1684661656493)(E:\csdn_blog博客\大三\上\计算机系统二\image\data.png)]

  • 小端存储
  • 初始化为0b不会出现在此处

关于.bss

已知其是存放未初始化的静态C变量(未初始化的全局变量放在COMMON中)和初始化为0的全局和静态变量

  • 观察节头部表可以发现,.bss.rodata的起始位置一样但大小不一样

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4a4tHmsE-1684661656494)(E:\csdn_blog博客\大三\上\计算机系统二\image\bss.png)]

    • 这是因为bss不占据实际的空间,他就是一个占位符,程序区分已初始化和未初始化的变量是为了节省空间,当程序运行是,会在内存中分配这些变量,并把初始值设为0

查看.rodata的内容

  • ro就是read only,代表只读,此区域就是存放只读数据的,例如printf语句中的格式串,或者是switch的跳转表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-baSsGYAK-1684661656494)(E:\csdn_blog博客\大三\上\计算机系统二\复习\image\rodata.png)]

  • 0x73对应115(d)对应s的ascll

关于其他的节表示的信息

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EbTykcgB-1684661656495)(E:\csdn_blog博客\大三\上\计算机系统二\复习\image\其他节信息.png)]

详解符号表

typedef struct{
    int name;  // 符号的名称  通过在字符串表中的字节偏移中得到
    char type:4,  // 该符号的类别  函数还是变量 
    	binding:4;  // 全局还是本地
    char reserved;  // 没有被用到
    short section;  // 符号所属目标文件的节
    long value;  // 符号的地址相对于所属节起始位置的偏移
    long size;	// 符号的大小
} Elf64_Symbol;
  • 符号是链接的粘合剂,整个链接过程是基于符号才能正确完成

readelf -s main.o

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OL5GoBVk-1684661656495)(E:\csdn_blog博客\大三\上\计算机系统二\复习\image\symtab.png)]

  • Ndx对应section,代表符号所属目标文件的节
    • Ndx的取值还有三个,他们分别是ABS、UNDEF、COMMON,他们被统称为伪节,他们在节头部表中没有条目。只有可重定位目标文件才有这些伪节,可执行目标文件中没有
      • ABS:不该被重定位的符号
      • UNDEF:未定义的符号,但是在其他文件有定义
      • COMMON:还未被分配位置的未初始化的数据目标,也就是未初始化的全局变量会存放于此
  • Value代表符号的地址相对于所属节起始位置的偏移
    • 例如func符号,属于.text节,value0x00,大小为36

符号

  • 全局符号

  • 外部符号

    • 本文件引用外部文件的变量
  • 局部符号

    • static是局部符号

编译器如何解析多重定义的全局符号

强弱符号的概念

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

处理多重定义的符号名

  • 规则一:不允许有多个同名符号
  • 规则二:如果有一个强符号多个若符号同名,那么选择强符号
  • 规则三:如果有多个弱符号同名,那么从这些弱符号中任意选择一个

对于这样一种情况:如果重复定义的符号是不同类型时,往往会破坏其他符号的内存

// foo1.cpp
#include<stdio.h>
void f();
int x = 15212;
int y = 15213;

int main()
{
    f();
    printf("x = 0x%x y = 0x%x \n", x, y);
    return 0;
}

// foo2.cpp
double x;
void f(){
    x = -0.0;
}
  • foo1xy的地址是连续的,被定义被int,占4个字节,但是在bar中,xdouble类型,占8个字节,在bar中对x赋值会影响y的值

静态库与静态链接

静态库:可以将多个相关的目标模块打包成一个单独的文件,称为静态库

  • 通过静态库,相关的函数可以被编译为独立的目标模块,然后封装成一个单独的静态库文件

  • 静态库可以用作链接器的输入。链接器在构造可执行文件时,从静态库中复制被应用程序引用的目标模块,其他未用到的模块则不会复制

  • 静态库的实例:

    • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tZQTRlAS-1684661656495)(E:\csdn_blog博客\大三\上\计算机系统二\复习\image\构造静态库.png)]

    • // main2.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;
      }
      
      // vector.h  其中的函数定义都在打包后的静态库中
      void addvec(int *x, int *y, int *z, int n);
      void multvec(int *x, int *y, int *z, int n);
      int getcount();
      

构造和使用一个静态库

  • > gcc -c addvec.c multvec.c  # 将想要打包的函数定义变成可重定位目标文件
    > ar rcs libvector.a addvec.o mutvec.o  # 打包成静态库
    > gcc -c main2.c   
    > gcc -static -o prog main2.o ./libvector.a  # 与静态库链接(使用静态库)
    
  • 总结:

    • 静态库就是各种可重定位文件的集合
    • 静态链接链接一个静态库的时候会按需链接

静态库的解析过程

  • 当在命令行输入以下命令来让程序使用静态库时

    gcc -static -o prog2c main2.o ./libvector.a

    链接器从左到右按照他们在编译器驱动程序命令行上出现的顺序来扫描可重定位目标文件存档文件,这会导致一些问题,最后来说。

  • 在链接器的扫描过程中,链接器维护三个集合

    • 可重定位目标文件的集合E(这个集合中的文件会被合并起来形成可执行文件)
    • 未解析的符号集合U(就是引用了但是没有定义,链接器会把他当做在其他文件定义)
    • 在前面的输入文件以及定义的符号集合D
  • 模拟解析过程

    • 第一个扫描的文件是main2.o,观察源程序可知,main.o会被放进集合E中,U中会增加main2.o中调用的addvecprintfD中会增加已定义的函数main和变量x、y、z

    • 第二个扫描的文件是libvector.a,根据其后缀,可知其为一个静态库,此时链接器就会尝试在U集合中寻找是否有与静态库同名的变量或函数,由于U中有addvec,故匹配,删除U集合中的addvec,将addvec.o放入E集合中,然后将addvec.o中定义的符号放进符号D

    • libvector.a中包含的所有目标文件要执行上述操作

    • 任何不包含在集合E中的成员目标文件都被简单的丢弃

  • 未定义的原因

    • 如果在最后一个目标文件读取完成之后,U集合不为空,则会产生未定义的情况。
  • 该算法的缺陷

    • 由于链接器是按照顺序从前往后的,故如果此指令gcc -static -o prog2c main2.o ./libvector.amain与静态库调换顺序,当扫描静态库时U集合并没有元素,故main扫描后U中符号无法消除就会产生未定义的情况
  • 互相依赖的命令

    • foo.c调用libx.alibx.a调用liby.a,然后liby.a又调用libx.a

      gcc -static foo.c libx.a liby.a libx.a

重定位

重定位节(section)和重定位符号定义

  • 对于main.osum.o,他们相同类型的section会被合并为一个新的section
  • 观察main.osum.o的符号表,他们的.text section都是从0开始的
  • 64linux系统中,ELF可执行文件默认从地址0x400000处开始分配,人们

重定位节中的符号引用

  • 本步骤是确定那些调用外部函数的目的地址(就是指令编码后面的的字节,汇编器把他们都填充成0,由链接器来赋值)

  • 链接器要依赖可重定位条目的数据结构来决定目的地址的值

  • 当编译器遇到最终位置不确定的符号引用时,他就产生一个重定位条目

    typedef struct{
        long offset;    //需要被修改的引用的节偏移(即该符号引用距离所在节的初始位置的偏移)。
        long type:32,   //重定位类型,不同的重定位类型会用不同的方式来修改引用
             symbol:32; //指向的符号,比如sum
        long addend;    //一个有符号常数,一些类型的重定位要使用它对被修改引用的值做偏移调整  pc相对寻址中默认是-4  绝对寻址默认是 0   
    }
    

    关于type字段,只学习两种即可,

    • R_X86_64_PC32PC相对地址

    • R_X86_64_32:绝对地址

  • 给出重定位之后的汇编代码,解释

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FsvAa8ll-1684661656496)(E:\csdn_blog博客\大三\上\计算机系统二\复习\image\重定位后.png)]

    当执行到call指令时,PC0x4004e3,目标地址就是PC地址加0x00000005,这个5就是链接器通过重定位条目所计算出来的

    如何计算的?

    // 对于这样两个函数
    // main.c
    int sum(int *a, int n);
    
    int array[2] = {1, 2};
    
    int main(){
        int val = sum(array, 2);
        return val;
    }
    
    // sum.c
    int sum(int * a, int n){
        int i, s = 0;
        
        for(i = 0; i < n; i++){
            s += a[i];
        }
        return s;
    }
    
    
    // 已经确定重定位后的.text节和sum函数的绝对地址分别为 0x4004d0 和 0x4004e8	`
    
    // 重定位之前的汇编代码(在合成之前main.o或者sum.o的.text起始地址都是0)
    000000000000<main>:
    0:  48 83 ec 08			sub $0x8, %rsp
    4:  be 02 00 00 00		mov $0x2, %esi
    9:  bf 00 00 00 00		moV $0×0, %edi
    e:  e8 00 00 00 00 		callq 13 <main+13>
    13: 48 83 c4 08			add $0x8, %rsp
    17: c3					retq
    
    // call后的目标位置值为f,表示与所在节的初始位置的偏移值为f,所以计算  目标位置的值为  sum - addend - (main + offset)
        			 0x4004e8 - 4 - (0x4004d0 + f) = 0x5
    
    // 所以
    e:  e8 00 00 00 00 		callq 13 <main+13>
    // 变为
    e:  e8 05 00 00 00      callq 4004e8 <sum>
    

可执行目标文件

可执行目标文件是一个二进制文件

  • 可执行目标文件的格式与可重定位目标文件的格式类似

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LY8kgsJQ-1684661656496)(E:\csdn_blog博客\大三\上\计算机系统二\复习\image\可执行目标文件.png)]

    • ELF头部描述文件的总体格式,还包括程序的入口点也就是当程序运行时要执行的第一条地址
    • .init节定义了一个小函数,叫做_init,程序的初始化代码会调用它

加载可执行目标文件

  • 任何Linux程序都可以通过调用execve函数来调用加载器。加载器将可执行文件的代码和数据从磁盘复制到内存中,然后通过跳转到程序的第一条指令或入口来运行该程序。这个将程序复制到内存并运行的过程叫做加载

  • 每个Linux程序都有一个运行时内存映像,内存映像如图所示

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o1mtvH9q-1684661656497)(E:\csdn_blog博客\大三\上\计算机系统二\复习\image\内存映像.png)]

    • Linux x86-64系统中,代码段总是从地址0x400000处开始后面是数据段,
    • 运行时堆在数据段之后,通过调用malloc库往上增长
    • 堆后的区域是为共享模块保留的
    • 用户栈总是从最大的合法用户地址(248 - 1)开始,向小内存地址增长。
    • 内核就是操作系统驻留在内存的部分
    • 注意:
      • 为了简洁,将代码段与数据段挨在了一起,事实上,.data段是有对齐要求的。所以代码段和数据段之间是有间隙的。
      • 同时,在分配栈、共享库和堆段运行时地址时,链接器还会使用地址空间布局随机化。但是他们的相对位置不会变

加载运行过程细节

  • 当加载器运行时,在程序头部表的引导下,加载器将可执行文件的片复制到代码段和数据段。

  • 然后,加载器跳转到程序的入口点(_start函数的地址),这个函数是在系统目标文件ctrl.o中定义的

  • start函数调用系统启动函数 _ _libc_start_main,该函数定义在libc.so中。

  • 上一函数初始化执行环境,调用用户层的main函数,再由

    _ _libc_start_main处理main函数的返回值,并且它在需要的时候返回给内核

共享库

是一种特殊的可重定位目标~文件

构造一个共享库

> gcc -shared -fpic -o libvector.so addvec.c mulvec.c
  • -fpic:告诉编译器生成与位置无关的代码

使用共享库

> gcc -o prog main.c ./libvector.so

解释共享库工作原理

  • 当创建可执行文件时,静态执行一些链接,然后在程序加载时,动态完成链接过程。
  • 没有任何libvector.so的代码和数据节真的被复制到可执行文件prog中,-> 链接器复制了一些重定位和符号表信息,他们使得运行时可以解析对libvector.so中代码和数据的引用
  • 当可执行程序prog被加载运行时,加载器会发现prog中存在一个名为.interpsection,这个section包含了动态链接器的路径名,这个动态链接器本身也是一个共享目标文件
  • 接下来,加载器会将这个动态链接器加载到内存中运行,然后由动态链接器执行重定位代码和数据的工作
    • 重定位libc.so的文本和数据到某个内存段
    • 重定位libvecor.so的文本和数据到另一个内存段
    • 重定位prog中所有由libc.solibvector.so定义的符号的引用
  • 之后共享库的位置就固定了,并且在程序执行的过程中都不会改变
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值