程序员的自我修养笔记

静态链接

库是一组目标文件的包,就是一些常用的代码编译成目标文件后打包存放。

第三章 目标文件里有什么

目标文件的格式

目标文件从结构上讲,它是已经编译后的可执行文件格式,只是还没有经过链接的过程,其中可能有些符号或者有些地址还没有被调整。

现在 PC 平台流形的可执行文件格式,主要是 Windows 下的 PE(Portable Executable)和 Linux 下的 ELF(Executable Linkable Format),它们都是 COFF(Common file format)格式的变种。

指令和数据分开存放的好处:

  • 一方面当程序被装载后,数据和指令分别被映射到两个虚存区域。由于数据区域对于进程来说是可读写的,而指令区域对于进程来说是只读的,所以这两个虚存区域的权限可以被设置成可读写和只读,这样可以防止程序的指令被有意或无意地改写。

  • 另一方面是现代 CPU 有强大的缓存体系,由于缓存很重要,所以程序必须尽量提高缓存命中率。指令区和数据区分离有利于提高程序的局部性。现代 CPU 的缓存一般都被设计成数据缓存和指令缓存,所以程序的指令和数据分开存放对于 CPU 的缓存命中率提高有好处。

  • 第三个原因,也是最重要的原因,就是当系统中运行着多个该进程副本时,他们的指令都是一样的,所以内存中只需要保存一份程序的指令部分。

真正了牛逼的程序员对自己的程序每一个字节都了如指掌。

objdump -h  SimpleSsection.o  # 打印elf文件各个段的信息
size SimpleSsection.o           # 查看elf文件各个段的长度
objdump -s -d SimpleSsection.o # -s将所有段内容以十六进制打印,-d将所有包含指令的段反汇编
段名称 内容
.data - 初始化的全局变量
- 局部静态变量
.rodata 只读数据段,对这个段的任何修改都是非法的,保证了程序的安全性。
有时候编译器会把字符串放到 data 段
- 只读变量 const 修饰
- 字符串常量
.bss 不占磁盘空间,
- 未初始化的全局变量
- 未初始化的局部静态变量
- 初始化为 0 的静态变量
.comment 存放编译器版本信息,比如字符串“GCC:(GNU)4.2.0”
.line 调试时的行号表,即源代码行号与编译后指令的对应表
.note 额外的编译器信息,如程序公司名,版本号
.symtab Symbol Table 符号表
.plt 动态链接的跳转表
.got 动态链接的全局入口表

段名称都是.前缀,表示这些表名字是系统保留的,应用程序也可以使用一些非系统保留的名字作为段名称。比如可以加入一个music段,里面存一首 mp3 音乐,运行起来后就会播放音乐,打算自定义段不能使用.作为前缀,以免与系统保留段名冲突。

Q: 如何将一个二进制文件,如图片,MP3 文件作为目标文件的一个段?
A: 可以使用 objcopy 工具,比如有一个图片 image..jpg,大小为 0x2100 字节:
$ objcopy -I binary -O elf32-i388 -B i38 image.jpg image.o

正常情况下编译出来的目标文件,代码会放到.text段,但是有时候你希望变量或者某些代码能放到你指定的段中去,以实现某些特定的功能。比如为了满足某些硬件的内存和 IO 地址布局。GCC 提供了扩展机制,使得程序员可以指定变量所处的段:

__attribute__((section("FOO"))) int global = 42;
__attribute__((section("BAR"))) void foo;

ELF 文件结构

使用readelf命令查看 elf 文件详细信息。

  • ELF 魔数,确认文件类型。

  • 文件类型

    常量 含义
    ET_REL 1 可重定位文件,一般问.o文件
    ET_EXEC 2 可执行文件
    ET_DYN 3 共享目标文件,一般为.so文件
  • 机器类型

    常量 含义
    EM_M32 1 AT&T WE 32100
    EM_SPARC 2 SPARC
    EM_M386 3 Intel x86
    EM_68K 4 Motorola 68000
    EM_88K 5 Motorola 88000
    EM_860 6 Intel 80860

段表是保存各个段的基本属性的结构。段表是除文件头外最重要的结构。编译器,链接器和装载器都是依靠段表来定位和访问各个段的属性。

链接的接口 - 符号

符号表结构

链接过程的本质就是要把多个不同的目标文件之间相互粘到一起。

目标文件 B 要用到目标文件 A 的函数foo,我们称目标文件 A定义了函数foo,目标文件 B引用了目标文件 A 的函数foo

链接中,我们将函数和变量统称为符号,函数名或变量名就是符号名。、

每一个目标文件都会有一个相应的符号表,表里记录了目标文件中所用到的所有符号。每个符号都有一个对应值,叫符号值,对于变量和函数来说,符号值就是他们的地址。

符号类型:

  • 定义在本目标文件的全局符号,可以被其他目标引用。
  • 在本目标文件中应用的全局符号,却没有定义在本目标文件。
  • 段名称,也就是段起始地址。
  • 局部符号,一些静态变量等。
  • 行号信息。

最重要的就是第一类和第二类。链接只关心全局符号的相互粘合,其他都是次要的。

可以使用 readelf objdump nm等命令查看符号信息。

特殊符号

一些特殊符号,没有在程序中定义,但是可以直接声明并引用它:

  • __executable_start,程序起始地址,不是入口地址,是程序最开始的地址。
  • __etext __etext etext 代码段结束地址,代码段最末尾的地址。
  • _edata edata 数据段结束地址,数据段最末尾地址。
  • __end end 程序结束地址。

符号修饰

符号应与对应的函数或者变量同名,但是在 C 语言发明时,已经存在了很多库和目标文件,如果再用一样的函数或变量就会冲突为了避免冲突,C 语言编译后符号名前会加上下划线_,如foo变成_foo,Fortran 语言编译后会在符号前后加上下划线_foo_

C++具有类,继承,重载等复杂机制,为了支持这些复杂特性,人们发明了符号修饰符号改编

函数签名包含了一个函数的信息,包括函数名,参数类型,所在类和名称空间等信息。它用于识别不同的函数。在编译器和链接器处理符号时,使用某种名称修饰的方法,是的每个函数签名对应一个修饰后名称

由于不同的编译器采用不同的名字修饰方式,必然导致由不同编译器编译产生的目标文件无法正常互相链接,这是导致不同编译器之间不能互操作的主要原因之一。

extern C

C++为了兼容 C,C++编译器会将在extern C 的大括号内部的代码当做 C 语言代码处理,这样就不会使用 C++的名称修饰机制。(也就不会在编译的时候加上下划线)

但是 C 语言并不支持extern C关键字,又不能为同一个库函数写两套头文件,这时候就可以用 C++的宏,__cplusplus。C++编译器会在编译 C++的程序时默认定义这个宏,我们可以用条件宏来判断当前编译单元是不是 C++代码。

#ifdef __cplusplus
extern "C" {
#endif

void *memset(void *, int , size_t);

#ifdef __cplusplus
}
#endif

弱符号与强符号

我们经常碰到符号重定义,多个目标文件中含有相同名字全局符号的定义,那么这些目标文件链接的时候就会出现符号重定义的错误。比如在两个文件中定义了相同的全局变量。

对于C/C++来说,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。

也可以使用 GCC 的__attribute__((weak))来定义任何一个强符号为弱符号。

  • 不允许强符号被多次定义,如果多次定义,则链接器报重复定义错误;
  • 如果一个符号在某文件中是强符号,在其他文件中都是弱符号,那么选择强符号。
  • 如果一个符号在所有目标文件中都是弱符号,那么选择其中占用空间最大的一个。

第四章 静态链接

空间地址分配

可执行文件中的代码段和数据段就是多个文件合并而来的,对于多个文件链接器如何将它们合并到输出文件?

按序叠加:最简单的方式,按照输入文件顺序依次合并。这会导致大量碎片,比如 x86 的硬件,段的装载地址和空间的对齐单位是页,也就是 4096 字节,那么如果一个段的长度只有 1 字节,它在内存里也要占用 4096 字节。

相似段合并:将所有相同性质的段合并在一起。

现在的链接器基本上采用第二种。使用这种方法的链接器都采用一种叫两步链接的方法。

第一步,空间与地址分配。扫描所有的输入目标文件,并且获得各个段的长度,属性和位置,并将输入目标文件中的符号表所有的符号定义和符号引用收集起来,统一放到一个全局符号表。这一步,链接器将能够获得所有输入目标文件的段长度,并且将他们合并,计算出输出文件中各个段合并后的长度和位置,并建立映射关系。

第二部,符号解析与重定位。使用上面收集到的信息,读取输入文件中段的数据,重定位信息。并且进行符号解析与重定位,调整代码中的地址。

VMA(Virtual Memory Address)虚拟地址,LMA(Load Memory Address)加载地址。正常情况这两个值是一样的。

链接之前目标文件的所有短 VMA 都是 0,因为虚拟空间还没有被分配,默认为 0,链接之后各个段就会被分配相应的虚拟地址。

Linux 下,ELF 可执行文件默认从地址0x8048000开始分配。

符号解析与重定位

objdump -d  查看代码段反汇编结果

源代码在编译成目标文件时并不知道函数的调用地址。需要通过链接时重定位。

链接器如何知道哪些指令需要被调整?这就用到了重定位表

重定位表就是 ELF 文件的一个段,所以其实重定位表也可以叫重定位段。

objdump -r 查看重定位表

每个要被重定位的地方叫一个重定位入口(Relocation Entry)。

重定位过程也伴随着符号的解析过程,每个目标文件都可能定义一些符号,或引用到定义在其他文件的符号。重定位过程中,每个重定位的入口都是对一个符号的引用,那么当链接器需要对某个符号的引用进行重定位时,他就要确定这个符号的目标地址。这时候链接器就会取查找由所有输入目标文件的符号表组成的全局符号表,找到对应的符号进行重定位。

readelf -s 查看符号表

对于 32 位 x86 平台下的 ELF 文件的重定位入口所修正的指令寻址方式只有两种:

  • 绝对近址 32 位寻址
  • 相对近址 32 位寻址

x86 基本重定位类型

宏定义 重定位修正方法
R_386_32 1 绝对寻址修正 S+A
R_386_PC32 2 相对寻址修正 S+A-P

A = 保存在被修正位置的值
P = 被修正的位置 (相对于段开始的偏移量或者虚拟地址),注意,该值可通过 r_offset 计算得到
S = 符号的实际地址,即由 r_info的高 24 位指定的符号的实际地址

第六章 可执行文件的装载与进程

程序运行时是有局部性原理的,所以我们可以将程序最常用的部分驻留在内存中,将不常用的数据存放在磁盘里,这就是动态载入的基本原理。

COMMON 块

Q:在目标文件中,编译器为什么不直接把未初始化的全局变量也当做未初始化的局部静态变量一样处理,为它在 BSS 段分配空间,而是将其标记为一个 COMMON 类型的变量?
A:当编译器将一个编译单元编译成目标文件时,如果该编译单元包含了弱符号(未初始化的全局变量就是典型),那么该弱符号最终所占大小未知,因为有可能其他编译单元中该符号所占空间比当前的大所以编译器此时无法为该符号在 BSS 段分配空间。但链接器在链接过程中可以确定弱符号大小,因为当链接器读取所有输入目标文件后,任何一个弱符号大小都可以确定,所以它可以在最终输出文件的 BSS 段为其分配空间。总体来看,未初始化全局变量最终还是被放在 BSS 段。

GCC 的-fno-common吧所有未初始化的全局变量不以 COMMON 块形式处理。

__attribute__扩展也可以实现,int global __attribute__((nocommon))。这样未初始化的全局变量就是强符号。

Q: 为什么静态运行库里面一个目标文件只包含一个函数?比如 libc.o 里面 printf.o 只包含 printf() 函数,strlen.o 只有 strlen 函数?
A:因为链接器在链接静态库时是以目标文件为单位的,比如我们引用了静态库中的 printf 函数,那么链接器就会把库中包含 printf 函数的那个目标文件链接进来,如果很多函数写在一个目标文件中,就将没用到的函数一起链接进了输出结果中。

链接的过程控制

第 6 章 可执行文件的装载与进程

程序运行时是有局部性原理的,所以我们可以将程序最常用的部分驻留在内存中,将不常用的数据存放在磁盘里,这就是动态载入的基本原理。

可执行文件在装载时实际上是被映射的虚拟空间,所以可执行文件又被叫做映像文件 (Image)。

Segment 和 Section 很难从中文翻译上区分,ELF 文件按 Section 存储的,从装载的角度 ELF 文件又可以按照 Segment 划分。

段地址对齐

可执行文件需要被装载,装载一般通过虚拟内存页映射机制完成,页是映射的最小单位,对于 x86 处理器来说,默认页大小为 4096 字节,所以内存空间的长度必须是 4096 的整数倍,并且这段空间在物理内存和进程虚拟地址空间的起始地址必须是 4096 的整数倍。

第 7 章 动态链接

第七章 动态链接

为什么要动态链接?

  • 内存和磁盘空间:如果两个程序都用到一个静态库,链接时就会有静态库的两个副本,运行时就会占用两份内存。
  • 程序的开发与发布:一个程序用到的静态库如果有更新,那么程序就需要重新链接,发布给用户。

要解决以上问题,最简单的方法就是把程序的模块相互分割开来,形成独立的文件,而不再将它们静态链接。就是不对目标文件进行链接,而等到程序运行时再链接。这就是动态链接的基本思想

动态链接模块的装载地址是从0x00000000开始的。

共享对象的最终装载地址在编译时是不确定的。

地址无关代码

静态共享库:将程序的各个模块交给操作系统管理,操作系统在某个特定的地址划分出一些地址块,为那些已知的模块预留足够的空间。

装载时重定位:程序在编译时被装载的目标地址为0x1000,但是在装载时操作系统发现0x1000这个地址已经被别的程序使用了,从0x4000开始有一块足够大的空间可以容纳,那么该程序就可以被装载至0x4000,程序指令和数据所有引用都只需要加上0x3000偏移量即可。因为他们在程序中的相对位置是不会改变的。

地址无关代码为了解决共享对象指令中对绝对地址的重定位问题,基本想法是把指令中那些需要被修改的部分分离出来,跟数据部分放在一起,这样指令部分就可以保持不变,而数据部分可以在每个进程中拥有一个副本。

模块中四类地址引用:


模块内部调用或者跳转
不需要重定位,本身就是地址无关的。

模块内部数据访问
指令中不能包含数据的绝对地址,所以使用相对寻址的方式。

模块间数据访问
把跟地址相关的部分放到数据段里面。ELF 的做法是在数据段里面建立一个指向这些数据的指针数据,称为全局偏移表(GOT)。当代码需要引用全局变量时,可以通过 GOT 间接引用。

链接器在装载时会查找每个变量的地址,填充 GOT 每个项,当指令中需要访问变量时,程序会先找到 GOT,根据 GOT 中对应的地址,找到对应的变量。GOT 本身放在数据段,所以他可以在模块装载时被修改,并且每个进程有独立副本,相互不影响。

以访问变量 b 为例,程序首先计算出变量 b 的地址在 GOT 中的位置,即

0x10000000 + 0x454 + 0x118c + 0xfffffff8 = 0x100015d8

0xfffffff8-8的补码表示,然后使用寄存器间接寻址方式给变量 b 赋值 2。

模块间调用跳转
类似于模块机数据访问,不同的是 GOT 中相应项保存的是目标函数的地址。


各种地址引用方式

指令跳转,调用 数据访问
模块内部 相对跳转和调用 相对地址访问
模块外部 间接跳转和调用(GOT) 间接访问(GOT)

Q : -fpic 和-fPIC 的区别?
A: 都是 GCC 产生地址无关代码的参数。-fpic产生的代码较小,-fPIC产生的代码较大。因为地址无关代码和硬件平台相关,在一些平台上-fpic会受到限制,比如全局符号的数量或者代码长度等,而后者没有这样的限制。

Q: 如果一个共享对象 lib.so 中定义了一个全局变量 G,进程 A 和进程 B 都是用了 lib.so。那么当进程 A 改变这个全局变量时,进程 B 的 G 是否受到影响?
A: 不会,应当 lib.so 被加载时,它的数据段部分在每个进程都有独立的副本。如果是同一个进程里的线程 A 和线程 B,那么他们是共享数据 G 的。

如果代码不是地址无关的,它就不能被多个进程共享,就失去了节省内存的优点。但是装载是重定位的共享对象的运行速度要比使用地址无关代码的共享对象快,因为它省去了地址无关代码中每次访问全局数据和函数是需要做一次计算当前地址以及间接地址寻址的过程。

延迟绑定 PLT

动态链接要比静态链接慢,一是因为动态链接下,对全局和静态数据的访问都要进行复杂的 GOT 定位,然后间接寻址。另外,程序开始执行时,动态链接器都要进行一次链接工作。

而在一个程序运行过程中,可能很多函数在程序执行完时都不会用到,如果一开始就把所有函数链接好实际就是一种浪费,所有 ELF 采用了一种叫做延迟绑定的做法,基本思想就是当函数第一次使用时才进行绑定(符号查找,重定位等)。

ELF 使用 PLT(Procedure Linkage Table)来实现延迟绑定。以调用bar()函数为例,之前的做法是通过 GOT 中的相应项进行跳转,而延迟绑定下,在这过程中间加了一层 PLT 间接跳转。每个外部函数在 PLT 中都有一个对应项,比如bar()在 PLT 中项的地址为bar@plt

bar@plt:
    jmp *(bar@GOT)
    push n
    push moduleID
    jump _dl_runtime_resolve

第一条是指令通过 GOT 间接跳转的指令,如果链接器在初始化阶段已经初始化该项,并将bar()地址填入该项,那么就能正确跳转到bar()。但是为了延迟绑定,链接器初始化时并没有将bar()地址填入,而是将第二条指令push n的地址填入了bar@GOT中,这一步不需要查找符号,代价很低。

第一条指令的效果就是跳转到第二条指令,第二条指令将数字n压入堆栈,这个数字是bar这个符号引用在重定位表.rel.plt中的下标。第三条指令将模块 ID 压入堆栈,最后跳转到_dl_runtime_resolve

_dl_runtime_resolve进行一系列工作后将bar()真正地址填入到bar@GOT

一旦bar()这个函数被解析完,当面再次调用bar@plt时,第一条jump指令就能跳转到bar()的真正地址。bar()函数返回时根据堆栈里保存的EIP直接返回到调用者,而不会执行bar@plt中第二条指令。那段代码只会在符号未被解析时执行一次

PLT 在 ELF 文件中以独立段存在,段名通常叫做.plt,因为它本身是一些地址无关的代码,所以可以跟代码段合并成同一个可读可执行的 Segment 被装载入内存。

动态链接相关结构

.interp 段

objdump -s a.out

Contents of section .interp:
804811 2f6c6962 2f6c696d 6c696e78 782e736f  /lib/ld-linux.so.2

里面保存的就是可执行文件所需要的动态链接器的路径,在 Linux 下,可执行文件动态链接器几乎都是/lib/ld-linux.so.2

这是个软链接,会他会指向系统中安装的动态链接器。当系统中的 Glibc 库更新时,软链接也会指向新的动态链接器,所以.interp段不需要修改。

可以通过以下命令查看可执行文件需要的动态链接器的路径:

$ readelf -l a.out | grep interpreter
    [Requesting program interpreter: /lib/ld-linux.so.2]

.dynamic 段

动态链接 ELF 中最重要的结构,这里保存了动态链接器所需要的基本信息,如依赖哪些共享对象,动态链接符号表的位置,动态链接重定位表的位置,共享对象初始化代码的地址等。

动态符号表

Program1程序一来Lib.so,引用到了里面的foobar()函数,那么对于Program1来说,称Program1导入(Import)了foobar函数,foobarProgram1的导入函数。

而站在Lib.so角度来说,它定义了foobar函数,我们称Lib.so导出(Export)了foobar函数,foobarLib.so的导出函数。

为了表示动态链接这些模块之间的符号导入导出关系,ELF 专门有一个叫做动态符号表的段来保存这些信息,段名通常叫.dynsym

.dynsym只保存与动态链接相关的符号,对于那些模块内部的符号,比如模块私有变量则不保存。

动态链接重定位表

PIC 模式的共享对象也需要重定位。

对于使用 PIC 技术的可执行文件或共享对象来说,虽然代码段不需要重定位,但是数据段还包含了绝对地址的引用,因为代码段中绝对地址相关的部分被分离出来,变成了 GOT,而 GOT 实际上是数据段的一部分。

目标文件的重定位在静态链接时完成,共享对象的重定位在装载时完成。

目标文件里包含专门用于重定位信息的重定位表,比如.rel.text表示是代码段重定位表,.rel.data表示数据段重定位表。

共享对象里类似的重定位表叫做.rel.dyn.rel.plt.rel.dyn实际上是对数据引用的修正,它所修正的位置位于.got以及数据段;.rel.plt实际上是对代码引用的修正,它所修正的位置位于.got.plt

用以下命令可以查看重定位表;

printf这个重定位入口,它的类型为R_386_JUMP_SLOT,它的偏移为0x000015d8。它实际位于.got.plt中,前三项是被系统占用的,第四项开始才是真正存放导入函数地址的地方,刚好是0x000015c8 + 4 * 3 = 0x000015d4,即__gmon_start__

当动态链接器要进行重定位时,先查找printf的地址,假设链接器在全局符号表中找到printf的地址为0x08801234,那么链接器就会将这个地址填入.got.plt中偏移为0x000015d8的位置。从而实现了地址重定位,即动态链接最关键的一步

动态链接时进程堆栈初始化信息

动态链接的步骤和实现

动态链接分为三步:启动动态链接本身,装载所有的共享对象,重定位和初始化。

Q:动态链接器本身是动态链接还是静态链接?
A:动态链接器本身应该是静态链接的,它不能依赖于其他共享对象,动态链接器本身使用来帮助其他 ELF 文件解决共享对象依赖问题的,如果它也依赖其他共享对象,那就陷入矛盾了。

Q:动态链接器本身必须是 PIC 的吗?
A:动态链接器可以是 PIC 的也可以不是,但是往往用 PIC 会简单一些。

Q:动态链接器可以被当做可执行文件运行,那么它的装载地址是多少?
A:ld.so 的装载地址跟一般的共享对象一样,即0x00000000。这个装载地址是一个无效的装载地址,作为一个共享库,内核在装载它时会为其选择一个合适的装载地址。

显示运行时链接

第 10 章 内存

程序的内存布局

在 32 位操作系统里,有 4GB 的寻址能力,大部分操作系统会将一部分挪给内核使用,应用程序无法直接访问这段内存。这部分称为内核空间。Windows 默认将高地址的 2GB 分给内核,Linux 默认分 1GB 给内核。

剩下的称为用户空间,在用户空间里也有一些特殊的地址区间:

  • 栈:维护函数调用上下文,通常在用户空间的最高地址处分配。
  • 堆:用来容纳程序动态分配的内存区域,当使用 malloc 或者 new 分配内存时,得到的内存来自于堆。通常在栈下方。
  • 可执行文件映像:存储可执行文件再内存里的映像,由装载器在装载时将可执行文件的内存读取活映射到这里。
  • 保留区:保留区并不是一个单一的内存区域,而是对内存中受到保护而禁止访问的内存区域的总称。比如NULL

栈与调用惯例

栈保存了一个函数调用所需要的维护信息,通常这被称为栈帧。一般包括如下几个方面:

  • 函数的返回地址和参数
  • 临时变量
  • 保存的上下文

一个函数的调用流程:

I386 标准函数进入和退出指令序列,基本形式:

push ebp
mov ebp, esp
sub esp, x
[push reg1]
...
[push regn]

函数实际内存

[pop regn]
...
[pop reg1]
mov esp, ebp
pop ebp
ret

Hot Patch Prologue 热补丁
在 Windows 函数里,有些函数尽管使用了标准的进入指令序列,但是在这些指令之前却插入了一些特殊内容:

mov edi,edi

这条指令没有任何用,在汇编之后会成为一个占用 2 字节的机器码,纯粹为了占位符而存在,使用这条指令开头的函数整体看起来是这样的:

nop
nop
nop
nop
nop
FUNCTION:
mov edi,edi
push ebp
mov ebp, esp

其中 nop 占 1 个字节,也是占位符,FUNCTION 为一个标号,表示函数入口,本身不占空间。

设计成这样的函数在运行时可以很容易被其他函数替换掉,在上面的指令序列中调用的函数是 FUNCTION,但是我们可以做一些修改,可以在运行时刻意改成调用函数 REPLACEMENT_FUNCTION。首先在进程内存空间任意处写入 REPLACEMENT_FUNCTION 的定义:

REPLACEMENT_FUNCTION:
push ebp
mov ebp, esp
...
mov esp, ebp
pop ebp
ret

然后修改原函数的内容:

LABEL:         # 标号不占字节
jmp REPLACEMENT_FUNCTION # 占5字节,刚好五个nop
FUNCTION:      # 函数入口标号,不占字节
jmp LABEL      # 近跳指令,占2字节,跳跃到上方,即使截获失败也不影响原函数执行
push ebp
mov ebp, esp
...

将 5 个nop换成一个jmp指令,然后将占用两个字节的mov edi,edi换成另一个jmp指令。因为这个jmp指令跳转的距离非常近,因此被汇编器翻译成了一个“近跳”指令,这种指令只占用两个字节。但只能跳跃到当前地址前后 127 个字节范围的目标位置

这里的替换机制,可以实现一种叫做钩子(HOOK)的技术,允许用户在某时刻截获特定函数的调用。


函数传递参数时压栈顺序,传递参数是寄存器传参还是栈传参等等都需要遵守一定的约定,否则函数将无法正确执行,这样的约定称为调用惯例

一个调用惯例一般会规定如下几个方面:

  • 函数参数的传递顺序和方式
    • 调用方压栈,函数自己从栈用取参数
    • 调用方压栈顺序:从左至右,还是从右至左?
  • 栈的维护方式
    • 参数出栈,可以由调用方完成还是由函数自己完成?
  • 名字修饰的策略
    • 为了链接的时候对调用惯例进行区分,调用惯例要对函数本身的名字进行修饰,不同调用惯例有不同的名字修饰策略
    • 没有显示指定调用惯例的函数默认是cdecl惯例
int _cdecl foo(int n, float m)

_cdel 是非标准关键字,在不同编译器中写法不同,在 gcc 中使用的是__attribute__((cdecl))

附录

文件名

英文名 Linux 扩展名 英文名 Windows 扩展名 功能
DSO-Dynamic Shared Objects ELF 动态链接文件,动态共享对象,共享对象 .so DLL-Dynamic Linking Library 动态链接库 .dll 1111
Static Shared Library 静态共享库 2222 2222 2222 2222 2222
1111 1111 1111 1111 1111 1111 1111
1111 1111 1111 1111 1111 1111 1111
1111 1111 1111 1111 1111 1111 1111