程序员的自我修养(一)计算机概念、静态链接、目标文件

第一章 计算机概念基础

1.1 硬件基础

1.2 软件基础

第二章 静态链接 ——编译和链接

2.1 编译过程

2.2 编译器

2.3 链接器

第三章 目标文件

3.1 EFL文件格式

3.2 EFL文件实例

3.3 EFL 文件详细内容

3.4 链接的接口——符号

3.5 调试信息

3.6 小结

第一章 计算机概念基础

1.1 硬件基础

  1. PC机: 兼容x86指令集的32位cpu的个人计算机

  2. 关键部件: 中央处理器CPU 内存 I/O控制芯片

  3. 芯片

    北桥芯片 设备高速交换数据:协调CPU、内存、高速图形设备

    南桥芯片 处理低速设备:磁盘、USB、键盘、鼠标

    南桥汇总后链接到北桥

    20世纪90年代PC机采用PCI/ISA及南北桥设计的硬件构架。之后有发明了AGP, PCI Express等总线结构和相应控制芯片。

  4. SMP与多核

    CPU的频率经历过一段时间的增加后到达了4GHz的天花板(工艺限制)。因此出现了多CPU计算器,常见的形势如 对称多处理器 (SMP)。即多CPU在系统中所处地位和发挥功能相同且对称。

    多CPU对运算速度的提升不是线性的,因为一个问题无法分解为若干不相干的子问题。常用情况:数据库,网络服务器,所以多处理器的成本很高。

    多核处理器(Multi-core Processor)是对于SMP的简化。不同的处理器之间共享昂贵的缓存,成本较多处理器降低很多。

1.2 软件基础

  1. 系统软件

    • 定义:管理计算机本身的软件

    • 分类:平台性(操作系统内核、驱动程序、运行库)

      ​ 程序开发(编译器、汇编器、连接器)

    • 开发程序 应用程序

      ↓使用者

      <应用程序编程接口(API)>

      ↑提供者

      运行库(Runtime Library)。比如Windows运行库提供windousAPI,Linux Glibc库提供POSIXAPI

      ↓使用者

      <系统调用接口(System call interface)>

      ↑提供者

      操作系统(Operating System Kernel) 提供软件中断。如Linux使用0x80号中断作为SCI

      ↓使用者

      <硬件规格(Hardware Specification)>

  2. 操作系统 Operating System

    • 功能:1) 提供抽象接口

      ​ 2) 管理硬件资源

    • 操作系统会CPU分配方式:

      CPU分配一直在发展,现在多为多任务(Multi-tasking)系统。所有的应用程序都以进程(Process)的方式运行(进程的总体目标是每个进程从逻辑上来看都可独占计算机资源)。CPU分配方式是抢占式(Preemptive),即在多个进程间快速切换。操作系统会将CPU资源分配给它所认为必要的进程。

    • 设备驱动

      操作系统直接控制硬件,比如向I/O端口0X1001写一个命令0X1111…… 这些表现在开发程序层面往往就是一个LineTo()函数,开发者不再需要直接写硬件的命令,而是通过层层接口将函数转化为机械指令。

      操作系统将硬件抽象化成了概念。具体硬件细节是操作系统中的硬件驱动(Device Driver)程序来完成

  3. 程序在内存上的运行

    • 存在问题:1)地址空间不隔离,可能会造成恶意溢出,改写其他程序。

      ​ 2)内存使用效率低,当内存不够时,会有大量的数据换入换出,导致效率低下。

      ​ 3)程序运行地址不正确,重定位出错。

    • 解决方法:虚拟地址: 分段:通过映射(映射由操作系统完成),令超出部分非法,解决1,3。

      ​ 分页:Intel系列处理器支持4KB/4MB的页大小,一般使用4KB,由操作系统决定。虚拟空间页为虚拟页,物理内存页为物理页,磁盘中页叫磁盘页。以页为单位存取交换数据方便,并且操作系统可以为页设置权限,保证安全。

    • 虚拟地址的硬件实现 MMU(Memory Management Unit)

      CPU --Virthal Address(虚拟地址)–>MMU–Physical address–>Physical Memory

  4. 线程

    • 定义: 轻量级进程,程序执行流的最小单元。一个标准线程由线程ID、当前指针PC、寄存器集合和堆栈组成。一个进程由一到多个线程组成,各个线程共享程序内存空间。

    • 多线程优点:多个操作并行,发挥计算机的计算能力,在数据共享方面的效率高很多。

    • 线程调度:当线程数多于处理器数时,一个处理器会处理多于一个的线程,在处理器上切换不同的线程的行为称之为线程调度(Thread Schedule)。线程通常拥有三种状态:

      • 运行 (可运行的时间叫做时间片)
      • 就绪 线程可执行但cpu被占用
      • 等待 线程等待某时间发生,无法执行
    • 主流调度方法:

      • 优先级调度:调整优先级以提高效率并且防止Starvation,一般给予等待时间长的进程更高的优先级,防止其被饿死。并且根据进入等待状态的频繁程度调整优先级,比如IO密集型线程会得到更高的优先级防止其他的进程被饿死。用户也可以调整优先级。
      • 轮转法
    • 可抢占线程:时间片用尽后进行线程调度,为当前主流。但是可能造成调度世纪不确定造成的问题。

      不可抢占线程:只有线程主动放弃时间片或者试图等待时才会发成线程调度。

    • 线程的安全性:因为数据在有时是共享的,可能造成数据在切换两个线程时产生错误。有一些API提供了原子化数据访问,但是有一些复杂的操作没有这样可直接调用的API。

      • 解决方式:同步与锁;设置二元信号量;互斥量;临界区;条件变量

第二章 静态链接——编译和链接

2.1 编译过程

  1. 编译

    gcc hello.c  
    ./a.out
    

    预处理(prepressing), 编译(compilation),汇编(assembly),链接(linking)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eisK1Mhe-1581431483487)(http://47.101.139.155:23333/images/2020/02/06/Screen-Shot-2020-02-06-at-7.19.23-PM.png)]

  2. 预编译

    将hello.c 和 stdio.h 通过cpp预编译成.i

    $gcc -E hello.c -o hello.i
    /
    $cpp hello.c > hello.i
    
    • 展开#define,处理#if #ifdef…,#include

    • 删除注释

    • 添加行号文件标识,便于编译器使用

    • 保留#pragma,编译器使用

      我们可以通过查看预编译后文件来判断宏定义是否正确或头文件是否正确

  3. 编译

    词法分析、语法分析、语义分析、优化后生产相应的汇编代码文件

    $gcc -S hello.i -o hello.s
    $ /usr/lin/gcc/i486-linux-gnu/4.1/cc1 hello.c
    

    得到汇编输出文件hello.s。cc1是对于c来说的,对于C++为cc1plus,Java是jc1

    可将预编译和编译合并为一个步骤,使用cc1来完成这两个步骤

  4. 汇编

    汇编器(as):将汇编代码转变成机器指令。每一条汇编几乎对应一条机器指令。

    $as hello.s -o hello.o
    /
    $gcc -c hello.s -o hello.o
    /
    $gcc -c hello.c -o hello.o
    
  5. 链接

    将一大堆文件链接后才能得到可执行文件"a.out"

2.2 编译器

编译器:将高级语言翻译成机器语言。

过程:扫描、语法分析、语义分析、源代码优化、代码生成、目标代码优化

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kYAkf9hd-1581431483488)(http://47.101.139.155:23333/images/2020/02/06/Screen-Shot-2020-02-06-at-9.16.15-PM.png)]

  1. 词法分析

    scan时Finit State Machine(有限状态机)将源代码字符切割成Token(记号)并且分类(标识符放入符号表,数字字符串存放在文字表)。

    Token:关键字,标识符,字面量(数字、字符串)和特殊符号(加、等)

    lex程序:按照用户描述好的词法规则分割字符串。

  2. 语法分析

    **Grammar Parser(语法分析器)**对Token分析,产生Syntax Tree(语法树),采用Context-free Grammar。

    Syntax Tree是以expression为节点的树。

    yacc(Yet Another Compiler Compiler)可根据用户给定的语法规则对输入记号序列进行解析,从而构建语法树。对于不同语言,编译器开发者只需改变语法规则。

    语法分析只完成了对表达式的语言层面的分析,但是无法判断语句的真正意义

  3. 语义分析

    Semantic Analyzer(语义分析器):可以分析静态语义(Static Semantic),包括声明、类型的匹配,类型的转换。同时,动态语义(Dynamic Semantiv)只有在运行期才能确定。

  4. 中间语言生成

    Source Code Optimizer(源码级优化器):源码级别的一个优化过程。生成Intermediate Code(中间代码)。中间代码有很多类型,比较常见的是Three-address Code(三地址码)。

    Intermediate code 将编译器分为前端和后端,之前所描述的部分为前端,之后则为后端。

  5. 编译器后端

    后端包括:Code generator(代码生成器)和Target Code Optimizer(目标代码优化器)

    Code generator转化成目标机器代码

    Target Code Optimizer优化目标机器代码,选择合适的寻址方式、使用位移来代替乘法运算。

  6. conclusion

    现在的编译器随着CPU的进化而变得复杂,比如GCC编译器几乎支持所有CPU平台。

    生成了目标代码后,有一个问题:index和array的地址还没有确定。如果代码中有变量定义在其他模块,在最终运行时,他们都要在最终连接时才能确定。现在的编译器可将一个源代码文件编译成一个未连接的目标文件,然后由编译器最终将目标文件连接成可执行文件。

2.3 链接器

静态链接

链接:将一些指令对其他符号地址的引用进行修正,包括地址空间分配、符号决议、重定位(Relocation)。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0z0YLwG7-1581431483488)(http://47.101.139.155:23333/images/2020/02/06/Screen-Shot-2020-02-06-at-10.54.56-PM.png)]

第三章 Object File(目标文件)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uZLJoCva-1581431483488)(http://47.101.139.155:23333/images/2020/02/07/Screen-Shot-2020-02-07-at-7.23.06-PM.png)]

$file foobar.o  //查看类型

EFL(Executable Linkable Format): 可执行(目标)文件格式(Linex系统,windows是PE)

目标文件:源代码编译后但是没有进行链接的中间文件,和可执行文件的内容结构相似,采用一种格式储存。

3.1 EFL文件格式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EeRJev00-1581431483489)(http://47.101.139.155:23333/images/2020/02/07/Screen-Shot-2020-02-07-at-7.43.09-PM.png)]

  • 目标文件包含:编译后的机器指令代码、数据、连接时所需信息(符号表、调试信息、字符串)

    存储形式:节(Section)/段(Segment)注:余文皆为段

    • 文件头:文件属性、文件是否可执行、静态链接/动态链接及入口地址、目标硬件/操作系统。包含一个段表,描述文件中各个段的数组。

      文件头后就是各个段的内容。

    • 编译后的执行语句编译成机械代码:.text段

    • 已初始化的全局变量局部变量静态变量:.data段

    • 未初始化的全局变量/局部静态变量:.bss(默认为0,没必要在.data分配空间)(为未初始化的全局变量和局部静态变量的大小总和,无内容不占空间)

  • 程序源代码编译后被分为两种段:程序指令、程序数据。

    代码段:程序指令。数据段,.bss:程序数据

  • 指令数据分离存储的意义:

    • 数据rw- 指令r–
    • cashe数据指令分离
    • 指令共享——节约空间

3.2 EFL文件实例

  • -c 代表只编译不链接

    $gcc -c SimpleSection.c 
    

    我们会得到一个SimpleSection.o的目标文件

  • 我们可以通过objdump查看object内部结构

    $objdump -h SimpleSection.o
    

    -h 代表将EFL文件各个段的信息打印,-x则会打印更多信息。

    readelf是Linux的工具

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sHomLtFS-1581431483489)(http://47.101.139.155:23333/images/2020/02/07/Screen-Shot-2020-02-07-at-8.20.24-PM.png)]

如图,除了基础的.text/.data/.bss,还有.rodata(只读数据段),.comment(注释信息段)和.note.GNU-stack(堆栈提示段)。

  • 读这段object信息:

    • size是段长度,File off是段的位置(偏移量)
    • CONTENTS代表此段在文件中存在

    他们在ELF中的结构如图:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S2wh6xR8-1581431483489)(http://47.101.139.155:23333/images/2020/02/07/Screen-Shot-2020-02-07-at-8.30.12-PM.png)]

  • size指令:用来查看ELF文件各段长度

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ylYOZEj4-1581431483489)(http://47.101.139.155:23333/images/2020/02/07/Screen-Shot-2020-02-07-at-8.44.53-PM.png)]

3.3 ELF文件详细内容

代码段

objdump的**-s参数可以将所有段的内容以16进制打印,-d**可将所有包含指令的段反汇编

我们可将objdump输出中关于代码段的内容提取,分析内容。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C8t0b9ZR-1581431483490)(http://47.101.139.155:23333/images/2020/02/07/Screen-Shot-2020-02-07-at-8.49.58-PM.png)]

Contents of section 是.text以16进制打印。最左为偏移量,中间四列是十六进制内容,最右一列是.text段的ASCII码。

下面反汇编的结果,.text包含SimpleSection.c的func1()和main()指令。

0x55对应push %ebp 0xc3对应ret

.data .rodata

.bss

others

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TB3jk2tP-1581431483490)(http://47.101.139.155:23333/images/2020/02/07/Screen-Shot-2020-02-07-at-8.56.46-PM.png)]

文件头

readelf可用于详细查看ELF文件

-h查看文件头

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u6kiQX47-1581431483490)(http://47.101.139.155:23333/images/2020/02/07/Screen-Shot-2020-02-07-at-9.02.15-PM.png)]

ELF相关常数定义在“/usr/include/elf.h”

如图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y31OPgaV-1581431483491)(http://47.101.139.155:23333/images/2020/02/07/Screen-Shot-2020-02-07-at-9.12.45-PM.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6mIaCxpO-1581431483491)(http://47.101.139.155:23333/images/2020/02/07/Screen-Shot-2020-02-07-at-9.12.52-PM.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tC3xUdj1-1581431483491)(http://47.101.139.155:23333/images/2020/02/07/Screen-Shot-2020-02-07-at-9.12.56-PM.png)]

e_ident

  • 魔数(magic)

几乎所有可执行文件格式的最开始几个字节都是魔数。比如a.out开始是ox01 ox07等。

作用:确定文件类型。操作系统在加载可执行文件时会确定魔数是否正确,否则拒绝加载。

  • 版本(Version) 最新1.2

  • 文件类型 e_type

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d6Er4W1X-1581431483492)(http://47.101.139.155:23333/images/2020/02/07/Screen-Shot-2020-02-07-at-9.17.54-PM.png)]

  • 机械类型 e_machine

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K2F2STEW-1581431483492)(http://47.101.139.155:23333/images/2020/02/07/Screen-Shot-2020-02-07-at-9.20.48-PM.png)]

  • 段表

    保存段的基本属性(段名、段长度、文件偏移、读写权限、其他属性)

    编译器、链接器、装载器依靠段表定位访问各段属性

    查看方式:

    • objump -h #简要信息
    • **readelf -S ** #详细信息
  • 重定位表

    .rel.text (相对位置定位表)

  • 字符串表

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZregaVSz-1581431483492)(http://47.101.139.155:23333/images/2020/02/07/Screen-Shot-2020-02-07-at-9.30.54-PM.png)]

3.4 链接的接口——符号

在连接中,函数和变量统称为符号(Symbol),函数名和变量名就是符号名(Symbol Name)

每一个目标文件有一个对应的符号表(Symbol Table)。每一个符号都有一个相应的值,叫做符号值(Symbol Value)。对于变量、函数,符号值就是他们的地址。

符号分类

  • 定义在本目标文件的全局符号,可被其他目标文件引用:main, func1, global_init_var
  • *定义在其他文件中的全局符号:printf
  • 段名 该段的起始地址:.text,.data
  • 局部符号,只在本编译单元可见:static_var, statoc_var2。这些局部符号对于链接没有作用。
  • 行号信息 目标文件指令与源代码代码行的对应关系。(可选)、

1.2 重要,345对于其它目标文件不可见,在链接过程中无关紧要。

nm可用来查看符号

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BRBx0zCk-1581431483492)(http://47.101.139.155:23333/images/2020/02/07/Screen-Shot-2020-02-07-at-9.53.31-PM.png)]

符号表结构

符号表是.symtab

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yXlKOTzI-1581431483493)(http://47.101.139.155:23333/images/2020/02/07/Screen-Shot-2020-02-07-at-10.04.27-PM.png)]

特殊符号

在使用ld链接器时,有一些可以直接声明引用的特殊符号,这些符号被定义在ld链接器的链接脚本中。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-406FfPqg-1581431483493)(http://47.101.139.155:23333/images/2020/02/08/Screen-Shot-2020-02-08-at-10.10.34-PM.png)]

我们可以在程序中直接使用这些符号

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xfRyNDsA-1581431483493)(http://47.101.139.155:23333/images/2020/02/08/Screen-Shot-2020-02-08-at-10.17.31-PM.png)]

符号修饰机制与函数签名

不同的函数可能拥有相同姓名,但是他的其他信息,比如参数类型、所在类、函数名等不同,因此他们拥有不同的函数签名(Function Signature)。函数签名用于识别不同的函数。

编译器、链接器处理符号时,使用某种名称修改的方法,每个还是签名对应一个修改后名称。即在C++源代码编译成目标文件时,会将函数和变量名称进行修饰,形成符号名。

名称修改机制也用来防止静态变量名称冲突。比如main()和func()中都含有静态变量foo,区分这两个变量,就会分别修饰名称,产生不同的符号名。

不同编译器的名称修改机制不同。

extern “C”

extern "C"{
	int func(int);
	int var;
}

C++将花括号内部代码当做C。因此如图的静态变量会对符号进行C语言的变量修饰。一般VS平台下会在名称前加_,但是linux的gcc不会如此修饰。

当我们声明一些C语言的函数和全局变量时,在C++的编译单元中,这些函数和全局变量的符号会不能被链接,extern "C"在C中不被支持,所以我们需要先判断本编译单元是那种语言。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5VhkaQ2S-1581431483494)(http://47.101.139.155:23333/images/2020/02/08/Screen-Shot-2020-02-08-at-10.42.04-PM.png)]

_cplusplus是一种C++中定义的宏,判断此宏是否被定义可用以判断此编译单元的类型是C++与否。

强符号/弱符号

  1. 编译器默认函数/初始化后的全局变量为强符号,未初始化的全局变量为若符号。但是我们可以通过
__attribute__((weak)) weak2 = 2;
__attribute__((weakref)) void foo();

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K8mas60u-1581431483494)(http://47.101.139.155:23333/images/2020/02/08/Screen-Shot-2020-02-08-at-11.11.54-PM.png)]

如图:weak,weak2 为弱符号、strong,main()为强符号。ext是外部变量的引用,非强非弱。

  1. 链接器选择多次定义符号的原则:

    (1)强不允许多次,否则报符号重复定义错误

    (2)选强

    (3)若都为弱,则选择占用最大的一个

  2. 弱符号和弱引用的应用

    库中定义的弱符号可被用户定义的强符号覆盖,从而使程序使用自定义版本的库函数

    程序可对某扩展功能模块的引用定义为弱引用,将扩展模块和程序链接在一起时,功能模块可正常使用。去掉这些模块也可正常连接,更利于程序功能的组合。

    还可以判断链接到多线程库或者单线程库

    gcc -lpthread可连接到多线程库

3.5 调试信息

现代编译器都支持源代码级别的调试。在gcc时加上-g参数,readelf发现object file里有很多"debug"段。

ELF采用DWARF的标准调试信息格式,microsoft也有对应的格式叫codeview。debug段在目标文件和可执行文件中占有很大空间,甚至大过代码和数据本身。当开发完程序并发布时,我们可以去掉无用的调试信息。

$strip foo

3.6 小结

(1)深入了解object file的文件格式——ELF

(文件头 段表 重定位表 字符串表 符号表 调试表 代码段 数据段等段)

(2)可执行文件、目标文件、库。实际上都是一样基于段的文件或者文件的集合。源代码经编译,代码数据分类后存放在相应的段中,编译器(汇编器)将辅助性信息(符号、重定位信息)按照表的形式存放在目标文件里。表在一般情况下就是段。(调试表 === debug段)

(3)目标文件需要进一步通过符号/重定位信息链接,形成可使用程序。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
《程序员的自我修养:链,装载与库》是一本由林锐、郭晓东、郑蕾等人合著的计算机技术书籍,在该书中,作者从程序员的视角出发,对链、装载与库等概念进行了深入的阐述和解析。 在计算机编程中,链是指将各个源文件中的代码模块组合成一个可执行的程序的过程。链可以分为静态和动态链两种方式。静态是在编译时将所有代码模块合并成一个独立的可执行文件,而动态链是在运行时根据需要加载相应的代码模块。 装载是指将一个程序从磁盘上加载到内存中准备执行的过程。在装载过程中,操作系统会为程序分配内存空间,并将程序中的各个模块加载到相应的内存地址上。装载过程中还包括解析模块之间的引用关系,以及进行地址重定位等操作。 库是指一组可重用的代码模块,通过链和装载的方式被程序调用。库可以分为静态库和动态库。静态库是在编译时将库的代码链到程序中,使程序与库的代码合并为一个可执行文件。动态库则是在运行时通过动态链的方式加载并调用。 《程序员的自我修养:链,装载与库》对于理解链、装载和库的原理和机制具有极大的帮助。通过学习这些概念,程序员可以更好地优化代码结构和组织,提高程序的性能和可维护性。同时,了解链、装载和库的工作原理也对于进行调试和故障排除具有重要意义。 总之,链、装载与库是计算机编程中的重要概念,对于程序员来说掌握这些知识是非常必要的。《程序员的自我修养:链,装载与库》这本书提供了深入浅出的解释和实例,对于想要学习和掌握这些知识的程序员来说是一本非常有价值的参考书籍。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值