《程序员的自我修养》

时间:2022-05-09 11:10:03

《程序员的自我修养》这本书偏底层,来来回回读了有三四遍了,每一次都有新的收获,不过很快又会忘记,所以写下了这本书从17年12月份至今的全书的笔记,留作以后自己复习。

 

《程序员的自我修养》

 

第二章:编译和链接

源代码生成可执行文件经过了几个步骤:

预编译 编译 汇编 链接

 

每个步骤做的事分别有

预编译 主要处理#号开头的预编译指令 比如宏定义#define 对源代码作简单的字符替换

#include将包含的文件插入到该指令的位置 删除注释

 

编译 对源代码进行扫描 生成一个一个的记号 然后用这些记号生成语法树 再根据语法树生成中间代码 比较典型的有 三地址码 形式是 Z = X op Y 然后用中间代码生成目标机器代码 也就是汇编指令

 

汇编 对汇编指令进行查表 翻译成0101的机器语言

 

链接 将所有的可重定位目标文件链接 生成可执行目标文件

首先

 

自己怎么验证这些知识点的过程

预编译 用gcc –E 指令处理文件后 查看生成的.i文件 发现#include指令包含的文件确实插入到了源代码种 源代码增大了许多 从200个字节增大到20kb

编译 用gcc –s 指令处理后 发现生成的.s文件 从20k变成了1.3k 确定是生成中间代码时优化了源代码 里面都是汇编代码

汇编 用gcc –c 指令处理后 发现生成的.o文件 从1.3k又增大到了2.2k 此时的文件叫做可重定位文件

 

56页开始

目标文件

elf文件 和 虚拟地址空间

elf虽然叫做可执行文件格式,可是不只是可执行文件(/bin/bash)用可执行文件格式存储

elf文件(可执行文件格式) 比如:.o /bin/bash文件 .so文件

.s 文件用file查看 叫做 ASCII text 文件

 

目标文件长什么样:

目标文件分段储存信息,

  1. 开头是一个elf头文件.,这个头文件描述了整个文件的文件属性,//不确定//和一个段表Section Table(描述各个段的数组,每个段在文件中的偏移位置)
  2. .text存放程序指令

3.    .data段存放初始化了的全局变量或者静态局部变量

4.    .bss段初始化为0或未初始化的全局变量和静态局部变量 但是.bss段其实没有内容,是个预留的位置(未初始化的全局变量会在*COM* 强符号与弱符号的问题)

 

除了这三个常用段 还有常见的有

.rodata段 只读数据段 如:字符串常量 全局const变量

.comment段 编译器版本信息 如:GCC:(GNU)4.8.5

.symtab段 Symbol Table 符号表

 

 

ELF Header

查看ElF Header 的数据结构 /usr/include/elf.h

a.out的文件魔数开头 0x01,0x07是个跳转指令

e_type:表示ELF文件类型,每个类型对应一个常量,系统通过这个判断(P74)

e_entry: Entry point address入口地址,规定eld程序的入口虚拟地址

e_shoff:Start of section header 段表在文件中的偏移

e_ehsize Size of this header ELF文件头本身的大小

e_shnum: Number of section headers 段表描述符数量 等于ELF文件拥有的段的数量

 

到71页 ELF头文件结构 需要看看32位下的入口点地址

 

段表:

底层的数据结构是数组,类型是 ELF32_Shdr,也定义在/usr/include/elf.h

描述了各个段的信息:每个段的 段名,段的长度,在文件中的偏移,读写权限

 

重定位表

.rela.text 段 类型为sh_type 表示是一个重定位表

 

符号:函数和变量统称为符号 Symbol

符号分为全局符号和局部符号

全局符号有两类:

1.定义在本目标文件的全局符号,可以被别的目标文件引用

2.外部符号, 引用了,却没有在本目标文件中定义的全局符号

 

局部符号:只在编译单元内部可见 如staic的变量

 

P87

c++有符号修饰的机制这是为了解决多态中函数名相同但是需要产生不同的符号名用于区分

不同的编译器可能会有不同的符号修饰方法这也是为什么不同编译器产生的.o文件可能不能正常链接

extern "c" { } 编译器会将大括号内的内容按照c语言处理也就是使c++中的符号名修饰机制失去作用

 

 

P92

弱符号和强符号

  1. inti; 这个是什么意思呢?有可能是定义也有可能是声明,对吧?那么怎么确定它是定义还是声明呢需要到链接的时候才能确定,因为只有到那个时候编译器才能看到所有的目标文件。那在链接之前呢,编译器会给这个符号i一个属性就是弱符号属性
  2. inti = 0;这个一定是定义,因为对i进行了初始化所以这个符号在编译阶段就可以确定了这就是强符号强符号在所有目标文件中只可以出现一次(这就是编译器报的重复定义的错误)
  3. extern inti; 这既不是强符号也不是弱符号,这是一个外部变量的引用,也就是变量声明。 需要用编译器去查看一下,这产不产生符号。

 

强引用和弱引用

  1. 平时使用的声明就是强引用,强引用就是如果在所有的目标文件中没有找到符号的定义,链接时就会报错。
  2. 弱引用需要在引用前面加上 __attribute__((weakref))这个扩展关键字,即使没有找到弱引用的定义,链接时候也不会报错。但是运行的时候就会报错。弱引用一般是在库中使用,比如多线程pthread_create

    __attribute__((weakref)) void foo();

    If (foo) { foo(); }

    ------------------------------------------------------------------------------------------

    预处理编译汇编链接

    gcc–E hello.c –o hello.i

    处理源文件中以#开头的预编译指令

     

    编译:

    把预处理玩的文件进行一系列词法分析(将源代码的字符序列分割成一系列记号),语法分析(产生语法树,表达式不合法,括号不匹配,表达式中缺少操作符,这些错误都是在这一步被发现的),语义分析(声明,类型的匹配,类型的转换)及生成汇编代码

     

    链接:

    把每个模块组装起来

    分三步

    1. 地址和空间分配:    (1)输出的可执行文件中的空间的分配

      (2)装载后的虚拟地址中的虚拟地址空间的分配

        计算出输出文件中的各个段的长度与位置并建立映射关系(相似段合并)

    1. 符号解析:    
    2. 重定位:    将重定位表中需要重定位的外部符号进行重定位