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

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 深入理解计算机系统第七章知识点总结(下)

深入理解计算机系统第七章知识点总结(上)+https://developer.aliyun.com/article/1416624

符号

  • 全局符号
  • 外部符号
  • 本文件引用外部文件的变量
  • 局部符号
  • 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的值

静态库与静态链接

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

  • 通过静态库,相关的函数可以被编译为独立的目标模块,然后封装成一个单独的静态库文件
  • 静态库可以用作链接器的输入。链接器在构造可执行文件时,从静态库中复制被应用程序引用的目标模块,其他未用到的模块则不会复制
  • 静态库的实例:

// 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:绝对地址
  • 给出重定位之后的汇编代码,解释

    当执行到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>

可执行目标文件

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

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

加载可执行目标文件

  • 任何Linux程序都可以通过调用execve函数来调用加载器。加载器将可执行文件的代码和数据从磁盘复制到内存中,然后通过跳转到程序的第一条指令或入口来运行该程序。这个将程序复制到内存并运行的过程叫做加载
  • 每个Linux程序都有一个运行时内存映像,内存映像如图所示
  • 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定义的符号的引用
  • 之后共享库的位置就固定了,并且在程序执行的过程中都不会改变
相关文章
|
7月前
|
存储 编解码 并行计算
【软件设计师备考 专题 】计算机系统的组成、体系结构分类及特性
【软件设计师备考 专题 】计算机系统的组成、体系结构分类及特性
118 0
|
7月前
|
存储 C语言
深入理解计算机系统第七章知识点总结(上)
深入理解计算机系统第七章知识点总结
77 0
|
6月前
|
存储 数据库 芯片
计算机系统论述与相关概念-思维导图
计算机系统论述与相关概念-思维导图
|
4月前
|
存储 算法 安全
【第二章】软件设计师 之 操作系统基本原理
这篇文章是软件设计师备考资料的第二章,讲解了操作系统的基本原理,包括操作系统概述、进程状态转换、同步与互斥问题、PV操作、死锁问题与银行家算法、存储管理、文件系统和设备管理等关键知识点。
【第二章】软件设计师 之 操作系统基本原理
|
7月前
|
存储 缓存 并行计算
【软件设计师】计算机系统基础知识考点
【软件设计师】计算机系统基础知识考点
|
7月前
|
自然语言处理 算法 前端开发
【软件设计师备考 专题 】编译、解释系统的基础知识和基本工作原理
【软件设计师备考 专题 】编译、解释系统的基础知识和基本工作原理
102 1
|
存储
第六章 习题(6789B)【计算机系统结构】
第六章 习题(6789B)【计算机系统结构】
111 0
|
7月前
|
云安全 安全 搜索推荐
计算机操作基础知识点总结
计算机操作基础知识点总结
|
7月前
|
存储 缓存 网络虚拟化
深入理解计算机系统第九章知识点总结
深入理解计算机系统第九章知识点总结
118 0