Blog

.init与.fini

Content #

Linux系统下一般程序的入口是“_start”,这个函数是Linux系统库(Glibc)的一部分。当我们的程序与Glibc库链接在一起形成最终可执行文件以后,这个函数就是程序的初始化部分的入口,程序初始化部分完成一系列初始化过程之后,会调用main函数来执行程序的主体。在main函数执行完成以后,返回到初始化部分,它进行一些清理工作,然后结束进程。对于有些场合,程序的一些特定的操作必须在main函数之前被执行,还有一些操作必须在main函数之后被执行,其中很具有代表性的就是C++的全局对象的构造和析构函数。因此ELF文件还定义了两种特殊的段。

.init 该段里面保存的是可执行指令,它构成了进程的初始化代码。因此,当一个程序开始运行时,在main函数被调用之前,Glibc的初始化部分安排执行这个段的中的代码。

.fini 该段保存着进程终止代码指令。因此,当一个程序的main函数正常退出时, Glibc会安排执行这个段中的代码。

这两个段.init和.fini的存在有着特别的目的,如果一个函数放到.init段,在 main函数执行前系统就会执行它。同理,假如一个函数放到.fint段,在main函数返回后该函数就会被执行。利用这两个特性,C++的全局构造和析构函数就由此实现。

From #

程序员的自我修养

函数级别链接

Content #

由于现在的程序和库通常来讲都非常庞大,一个目标文件可能包含成千上百个函数或变量。当我们须要用到某个目标文件中的任意一个函数或变量时,就须要把它整个地链接进来,也就是说那些没有用到的函数也被一起链接了进来。这样的后果是链接输出文件会变得很大,所有用到的没用到的变量和函数都一起塞到了输出文件中。

VISUAL C++编译器提供了一个编译选项叫函数级别链接(Functional-Level Linking,/Gy),这个选项的作用就是让所有的函数都像前面模板函数一样,单独保存到一个段里面。当链接器须要用到某个函数时,它就将它合并到输出文件中,对于那些没有用的函数则将它们抛弃。这种做法可以很大程度上减小输出文件的长度,减少空间浪费。但是这个优化选项会减慢编译和链接过程,因为链接器须要计算各个函数之间的依赖关系,并且所有函数都保持到独立的段中,目标函数的段的数量大大增加,重定位过程也会因为段的数目的增加而变得复杂,目标文件随着段数目的增加也会变得相对较大。

GCC编译器也提供了类似的机制,它有两个选择分别是“-ffunction-sections”和“-fdata-sections”,这两个选项的作用就是将每个函数或变量分别保持到独立的段中。

From #

程序员的自我修养

GCC中COMMON块的操作

Content #

GCC的“-fno-common”也允许我们把所有未初始化的全局变量不以COMMON块的形式处理,或者使用“_attribute_”扩展:

int global __attribute__((nocommon));

From #

程序员的自我修养

COMMON类型的变量

VMA与LMA

Content #

VMA表示Virtual Memory Address,即虚拟地址, LMA表示Load Memory Address,即加载地址,正常情况下这两个值应该是一样的,但是在有些嵌入式系统中,特别是在那些程序放在ROM的系统中时,LMA和VMA是不相同的。

在链接之前,目标文件中的所有段的VMA都是0,因为虚拟空间还没有被分配,所以它们默认都为0。等到链接之后,可执行文件中的各个段都被分配到了相应的虚拟地址。

From #

程序员的自我修养

COMMON类型的变量

Content #

在目标文件中,编译器为什么不直接把未初始化的全局变量也当作未初始化的局部静态变量一样处理,为它在BSS段分配空间,而是将其标记为一个COMMON类型的变量?

当编译器将一个编译单元编译成目标文件的时候,如果该编译单元包含了弱符号(未初始化的全局变量就是典型的弱符号),那么该弱符号最终所占空间的大小在此时是未知的,因为有可能其他编译单元中该符号所占的空间比本编译单元该符号所占的空间要大。所以编译器此时无法为该弱符号在BSS段分配空间,因为所须要空间的大小未知。但是链接器在链接过程中可以确定弱符号的大小,因为当链接器读取所有输入目标文件以后,任何一个弱符号的最终大小都可以确定了,所以它可以在最终输出文件的BSS段为其分配空间。所以总体来看,未初始化全局变量最终还是被放在BSS段的。

一旦一个未初始化的全局变量不是以COMMON块的形式存在,那么它就相当于一个强符号,如果其他目标文件中还有同一个变量的强符号定义,链接时就会发生符号重复定义错误。

From #

程序员的自我修养

ELF重定位表结构(Elf32_Rel)

Content #

一个重定位表同时也是ELF的一个段,段的类型(sh_type)是“SHT_REL”。它的“sh_link”表示符号表的下标,它的“sh_info”表示它作用于哪个段。比如“.rel.text”作用于“.text”段,而“.text”段的下标为“1”,那么“.rel.text”的“sh_info”为“1”。

重定位表是一个Elf32_Rel结构的数组,Elf32_Rel定义如下:

/* Relocation table entry without addend (in section of type SHT_REL).  */
typedef struct
{
  Elf32_Addr	r_offset;	/* Address */
  Elf32_Word	r_info;		/* Relocation type and symbol index */
} Elf32_Rel;

r_offset #

重定位入口的偏移地址。可重定位文件中,该值表示生定位入口所要修正的位置的第一个字节相对于段起始的偏移。可执行文件或共享对象文件中,该值表示重定位入口所要修正的位置的第一个字节的虚拟地址。

r_info #

低8位表示重定位入口的类型,高24位表示重定位入口的符号在符号表中的下标。

From #

程序员的自我修养

Elf32_EHdr.e_ident

e_ident #

“Elf32_Ehdr”中的e_ident这个成员对应了readelf输出结果中的“Class”、“Data”、“Version”、“OS/ABI”和“ABI Version”这5个参数。

最开始的4个字节是所有ELF文件都必须相同的标识码,分别为0x7F、0x45、0x4c、 0x46,第一个字节对应ASCII字符里面的DEL控制符,后面3个字节刚好是ELF这3 个字母的ASCII码。这4个字节又被称为ELF文件的魔数,几乎所有的可执行文件格式的最开始的几个字节都是魔数。

From #

程序员的自我修养

路径依赖(Path Dependence)

Content #

路径依赖是指人类社会中的技术演进或制度变迁均有类似于物理学中的惯性,即一旦进入某一路径(无论是“好”还是“坏”)就可能对这种路径产生依赖。

人们一旦做了某种选择,就好比走上了一条不归之路,惯性的力量以及既得利益约束会使这一选择不断自我强化,并让你轻易走不出去。美国经济学家道格拉斯·诺斯,使用“路径依赖”理论成功地阐释了经济制度的演进,于1993年获得诺贝尔经济学奖。

制度变及技术演进,都存在着报酬递增和自我强化机制。这种机制使制度变迁一旦走上了某一条路径,它的既定方向会在以后的发展中得到自我强化。所以,“人们过去作出的选择决定了他们可能的选择”。

沿着既定的路径,经济和政治制度的变迁可能进入良性循环的轨道,迅速优化;也可能顺着原来的错误路径往下滑;弄得不好,它还会被锁定在某种无效率的状态之下。一旦进入了锁定状态,要脱身而出就会变得十分困难,往往需要借助外部效应,引入外生变量或依靠政权的变化,才能实现对原有方向的扭转。

对组织而言,一种制度形成后,会形成某个既得利益集团,出于对利益和所能付出的成本的考虑,他们对制度有强烈的要求,只有巩固和强化现有制度才能保障他们继续获得利益,哪怕新制度对全局更有效率。

对个人而言,一旦人们做出选择以后会不断地投入精力、金钱及各种物资,如果哪天发现自己选择的道路不合适也不会轻易改变,因为这样会使得自己在前期的巨大投入变得一文不值,这在经济学上叫“沉没成本”。

沉没成本是路径依赖的主要原因。

From #

https://www.rstk.cn/news/132910.html?action=onClick

马屁股与航天飞机

马屁股与航天飞机

Content #

你能想象马屁股和航天飞机之间会有什么关系吗?

1869年5月10日,联合太平洋铁路与中央太平洋铁路在美国犹他州的普鲁蒙托里角接轨,正式宣告横贯美国东西的大动脉建成。美国铁路两条铁轨之间的标准距离是4.85英尺,这是一个很奇怪的标准,究竟从何而来?

原来铁路是由建电车轨道的人设计的,而这个4.85英尺正是电车所用的标准。电车轨道标准又是从哪里来的呢?原来最先造电车的人以前是造马车的,而他们是用马车的轮宽做标准。

那么,马车为什么要用这个轮宽标准呢?因为那时候的马车如果用任何其他轮距的话,马车的轮子很快会在英国的老路上撞坏。

为什么?因为这些路上的辙迹的宽度为4.85英尺。

这些辙迹又是从何而来呢?答案是古罗马人定的,4.85英尺正是罗马战车的宽度。

接下来又要问,罗马人为什么用4.85英尺作为战车的轮距宽度呢?原因很简单,这是两匹拉战车的马的屁股的宽度。

在电视上看美国航天飞机立在发射台上时,你留意看,在它的燃料箱的两旁有两个火箭推进器,这些推进器是由设在犹他州的工厂所提供的。如果可能的话,这家工厂的工程师希望把这些推进器造得再胖一些,这样容量就会大一些,但是他们不可以。

为什么?因为这些推进器造好后要用火车从工厂运到发射点,路上要通过一些隧道,而这些隧道的宽度只比火车轨道的宽度宽了一点点。

故事是颇有趣的。从一定意义上说,今天世界上最先进的运输系统的设计,或许是由两千年前两匹战马的屁股宽度来决定的。历史惯性的力量是多么强大,要冲破由惯性形成的规则又是多么艰难。

路径依赖(Path Dependence)的现象在商业社会就更多了,连锁经营模式就是典型代表。

From #

https://zhuanlan.zhihu.com/p/467985205

sub:Linker

链接器 #

两步链接(Two-pass Linking) R_X86_64_PC32的重定位计算方式 LIBRARY_PATH和LD_LIBRARY_PATH的区别 patch code技术 Section与Segment 链接器定义的特殊符号 符号是链接中的粘合剂 控制链接过程的三种方法 ld默认的链接脚本 链接时不产生符号信息 动态共享对象(DSO,Dynamic Shared Objects)

静态链接 #

静态链接的三个重要Section 静态变量的重定位 符号解析 强符号与弱符号 将强符号定义为弱符号(GCC) 弱引用和强引用 将外部函数声明为弱引用(GCC) COMMON类型的变量 GCC中COMMON块的操作 函数级别链接 静态运行库中一个目标文件只包含一个函数

地址无关 #

地址无关代码(PIC, Position-independent Code) 产生地址无关代码的四种情形 模块内部的数据访问(PIC) 全局偏移表 (Global Offset Table, GOT) GOT的内容是运行时计算出来的 共享模块的全局变量 多进程或多线程访问共享模块中的全局变量

动态链接 #

装载时重定位(Load Time Relocation) 动态链接器的位置 动态链接器与.dynamic段 动态链接器查找共享库的顺序 不同ELF可执行文件的程序入口 -export-dynamic 次版本号交会问题(Minor-revision Rendezvous Problem) FHS规定的共享库的位置 ldconfig LD_PRELOAD LD_DEBUG 如何区分一个DSO是否为PIC PLT(Procedure Linkage Table) PLT在ELF中的实现

sub:Shared Library

运行库 #

glibc程序入口解析 _start和_exit末尾的hlt指令的作用 FILE结构、fd和内核对象