操作系统如何区分可执行文件之elf文件分析

为什么需要文件头

我们有没有想过这样一个问题,为什么Linux下的程序不能放在windows下运行,我们可能会说,系统调用的库不一样,操作系统提供的接口不一样肯定不行。所以我们可以写一个不含系统调用的代码

int main() {
    
    
	while(1);
	return 0;
}

这段代码是不含任何系统调用的,我们对其进行编译

gcc -c -o main.o main.c

-c的作用是编译、汇编到目标代码,不进行链接,也就是直接生成目标文件。

-o的作用是将输出的文件以指定文件名来存储,有同名文件存在时直接覆盖。

经过上面gcc的编译后,我们得到了main.o文件,目前为止,它还是个“半成品”。为什么这么说呢?因为它只是个目标文件,也称为待重定位文件,重定位指的是文件里面所用的符号还没有安排地址,这些符号的地址需要将来与其他目标文件“组成”一个可执行文件时再重新定位(编排地址),这里的符号就是指该目标文件中所调用的函数或使用的变量,而这里的“组成”就是指链接。这些符号一般是位于其他文件中,所以在编译时不能确定其地址,需要在所有目标文件都到齐了,将它们链接到一起时再重新定位(编排地址)。

我们可以用file命令检查一下 main.o 的状态

lovetzp@ubuntu:~/os/kernel$ file main.o
main.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

relocatable这个属性也揭示了他是一个可重新定位的文件

我们可以用 nm 命令查看 main.o

lovetzp@ubuntu:~/os/kernel$ nm main.o
0000000000000000 T main

nm用来显示二进制目标文件的符号表

-A 每个符号前显示文件名

-D 显示动态符号

-g 仅显示外部符号

-r 反序显示符号表

由于咱们的main.c过于简单,里面只有一个符号,即main,所以nm只列出了它的符号信息。

Linux下用于链接的程序时ld,链接有一个好处,可以指定最终生成的可执行文件的起始虚拟地址。它是用-Ttext参数来指定的,所以咱们可以执行以下命令完成链接。

ld main.o -Ttext 0xc0001500 -e main -o Kernel.bin

-Ttext 指定起始虚拟地址为0xc0001500,这个地址是设计好的,其中–o的意义也是指定输出的文件名,至于-e,和–entry一样,字面上的意思是用来指定程序的起始地址。注意,不要被迷惑了,虽然说是指定起始地址,但参数不仅可以是数字形式的地址,而且可以是符号名,这和汇编中的标号也是地址是一样的道理。总之它用来指定程序从哪里开始执行。

咱们可以不加 -e 试一试,报错了

lovetzp@ubuntu:~/os/kernel$ ld main.o -Ttext 0xc0001500 -o Kernel.bin
ld: warning: cannot find entry symbol _start; defaulting to 00000000c0001500

找不到默认的入口符号 _start,默认地址为00000000c0001500,一个程序总该有个入口地址,这个地址表示的是程序将从哪里开始执行。要知道,并不是程序体中的第一个字节就是程序的起始地址,因为在程序的开头可能有函数声明或数据定义,那计算机如何知道程序的入口在哪里呢?也就编译后的程序应该从哪句代码开始执行呢?这入口代码可说不准是哪一个了。由于程序内的地址是在链接阶段编排(也就是重定位)的,所以在链接阶段必须要明确入口地址才行,于是链接器规定,默认只把名为_start的函数作为程序的入口地址,即默认的entry symbol是_start,除非另行指定。所以我们也可以修改一下我们的函数

int _start() {
    
    
	while(1);
	return 0;
}

编译链接

gcc -c -o main.o main.c;
ld main.o -Ttext 0xc0001500 -e main -o Kernel.bin;

整个过程没出任何问题。是不是很神奇,一个C语言没有main函数居然执行起来了,

我们也可以直接编译链接

gcc –o test.bin main.c

test.bin是gcc直接生成的可执行文件,它的大小是4586字节。而kernel.bin是经过手动编译、链接这两个步骤完成的,其文件大小是1777字节。用nm查看这两个文件,test.bin多了很多符号,这些符号说明编译器在编译过程中为咱们引用了别的代码,这就是c运行库的功劳。

那我们为什么用这么长的篇幅来讲这些呢,是因为想说明一个问题,即使我们最小的一个程序需要运行,也是需要操作系统支持的,操作系统需要知道我们用户的程序入口在哪里,他才可以把我们的程序放入内存,然后把CS:IP指向这个入口地址的地方开始执行,而这个入口我们怎么告诉操作系统呢,这个就需要用到文件头,Windows下的可执行程序是PE格式(如果您想说的是EXE,不要搞混了,EXE是扩展名,属于文件名的一部分,只是名字的后缀,它并不是真正的格式),而Linux下的文件头格式是elf,所以他们的程序当然不能通用,linux的程序放在windows下,windows连他的入口地址都不知道在哪里又怎么执行呢。

二进制程序的运行方法

任何程序都需要被载入到内存后才能运行,这是CPU等其他硬件的运行机制决定的,我们若在该硬件系统上运行程序,不得不遵守这样那样的约束。应用程序是独立于操作系统的,它不会像操作系统那样,含着金钥匙,一出生就直接在内存中。它们通常位于磁盘等外存设备中,在使用时,需要从外存中将其调入到内存后才行。

那么如何去加载用户程序呢?

操作系统是程序,是软件,用户程序也是软件,用一个程序去调用另一个程序一点难度都没有,最最简单的办法,就是用jmp或call指令。我们的BIOS就是这样调用mbr的,我们的mbr就是这样调用loader的。但大家还记得不,BIOS调用mbr,mbr的地址是0x7c00,mbr调用loader,loader的地址是0x900。这两个地址是固定的,也就是说,我们目前的方法是很不灵活的,调用方需要提前和被调用方约定调用地址。

由于每个程序都是单独存在的,所以程序的入口地址信息需要与程序绑定,最简单的办法就是在程序文件中专门腾出个空间来写入这些程序的入口地址,主调程序在该程序文件的相应空间中将该程序的入口信息读出来,将其加载到相应的入口地址,跳转过去就行了。当然不仅仅只写入程序入口地址,能写的东西很多,比如为了给程序分配内存,至少还得需要知道程序的尺寸大小。但在哪里写入程序的入口地址呢?这便是文件头的由来,在程序文件的开头部分记载这类信息,而程序文件中除文件头外其余的部分则是之前的程序体。

这样一来,原先的纯二进制可执行文件加上新的文件头就形成了一种文件格式,不仅文件是这种形式,很多其他的传输协议也是文件头+文件体的形式。在程序中,程序头(也就是文件头)用来描述程序的布局等信息,它属于信息的信息,也就是元数据。由于程序文件中包含了程序头,好处是程序的入口地址等信息不需要写死,调用方中的调用代码可以变得通用,根据实际情况加载便可。但不好的地方是这些元信息不是代码,故不应该将其放在CPU上“执行”,所以程序就不再是纯粹的二进制可执行文件了,所以,将这种具有程序头格式的程序文件从外存读入到内存后,从该程序文件的程序头中读出入口地址,需要直接跳进入口地址执行,跨过程序头才行。

程序头可以自定义,只要我们按照自己定义的格式去解析就行。但是我们自己造一种程序头又何必呢,自己设计的文件头自己当然认识,但这毕竟不通用,我们需要选择一种流行的文件格式,咱们是在Linux下用C语言编程,其编译器gcc生成的是elf文件格式。

elf格式的二进制文件

ELF指的是Executable and Linkable Format,可执行链接格式。最初是由UNIX系统实验室(USL)作为应用程序二进制接口(ABI)而开发和发行的。工具接口标准委员会(TIS)选择了它作为IA32体系结构上不同操作系统之间的可移植二进制文件格式,于是它就发展成为了事实上的二进制文件格式标准。

先跟大家交待下,在ELF规范中,把符合ELF格式协议的文件统称为“目标文件”或ELF文件,这与我们平时所说的目标文件是不同的。

在大家平时的习惯中,咱们把编译后,但未经链接的文件称为目标文件,也称为待重定位文件(relocatable file),比如在Linux下用gcc –c参数生成的.o文件。而平时我们所说的ELF文件也是指经过编译链接后的二进制可执行文件,该文件能够直接运行。

为了避免混淆,咱们采用与ELF规范相同的命名方式,本节中所说的目标文件即指各种类型符合ELF规范的文件,如二进制可执行文件和Linux下.o结尾的目标文件和.so结尾的动态库文件。而待重定位文件,可以理解成咱们惯常所说的目标文件(如Linux下的.o文件)

elf眼中的目标文件

**待重定位文件(relocatable file)**待重定位文件就是常说的目标文件,属于源文件编译后但未完成链接的半成品,它被用于与其他目标文件合并链接,以构建出二进制可执行文件或动态链接库。为什么称其为“待重定位”文件呢?原因是在该目标文件中,如果引用了其他外部文件(其他目标文件或库文件)中定义的符号(变量或者函数统称为符号),在编译阶段只能先标识出一个符号名,该符号具体的地址还不能确定,因为不知道该符号是在哪个外部文件中,而该外部文件需要被重定位后才能确定文件内的符号地址,这些重定位的工作是需要在连接的过程中完成的

**共享目标文件(shared object file)**这就是我们常说的动态链接库。在可执行文件被加载的过程中被动态链接,成为程序代码的一部分

**可执行文件(executable file )**经过编译链接后的、可以直接运行的程序文件

ELF文件格式依然分为文件头和文件体两部分,只是该文件头相对稍显复杂,类似层次化结构,先用个ELF header从“全局上”给出程序文件的组织结构,概要出程序中其他头表的位置大小等信息,如程序头表的大小及位置、节头表的大小及位置。然后,各个段和节的位置、大小等信息再分别从“具体的”程序头表和节头表中予以说明。

ELF格式的作用体现在两方面,一是链接阶段,另一方面是运行阶段,故它们在文件中组织布局咱们也从这两方面展示,

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ro0pTAXG-1680062924893)(C:\Users\LoveSS\AppData\Roaming\Typora\typora-user-images\image-20230315144723695.png)]

有关elf的任何定义都可以在Linux系统的/usr/include/elf.h中找到,这里面的定义才是最全最权威的。

ELF header

Elf32_Ehdr

数据类型名称 字节大小 对齐 意义
Elf32_Half 2 2 无符号中等大小整数
Elf32_Word 4 4 无符号大整数
Elf32_Addr 4 4 无符号程序运行地址
Elf32_Off 4 4 无符号文件偏移量

C语言中的结构体能够很直观地表示物理内存结构,用结构体的形式展现一个数据结构是最合适不过的啦

#define EI_NIDENT (16)

typedef struct
{
    
    
  unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
  Elf32_Half    e_type;         /* Object file type */
  Elf32_Half    e_machine;      /* Architecture */
  Elf32_Word    e_version;      /* Object file version */
  Elf32_Addr    e_entry;        /* Entry point virtual address */
  Elf32_Off e_phoff;            /* Program header table file offset */
  Elf32_Off e_shoff;            /* Section header table file offset */
  Elf32_Word    e_flags;        /* Processor-specific flags */
  Elf32_Half    e_ehsize;       /* ELF header size in bytes */
  Elf32_Half    e_phentsize;    /* Program header table entry size */
  Elf32_Half    e_phnum;        /* Program header table entry count */
  Elf32_Half    e_shentsize;    /* Section header table entry size */
  Elf32_Half    e_shnum;        /* Section header table entry count */
  Elf32_Half    e_shstrndx;     /* Section header string table index */
} Elf32_Ehdr;

**e_ident[16]**是16字节大小的数组,用来表示elf字符等信息

e_ident数组成员 意义
e_ident[0]=0x7f
e_ident[1]=‘E’
e_ident[2]=‘L’
e_ident[3]=‘F’
这4位是固定的ELF文件的魔数
e_ident[4] 用来标识elf文件的类型
值为0表示该文件是不可识别类型
值为1表示该文件是32位elf格式的文件
值为2表示该文件是64位elf格式的文件
e_ident[5] 指定编码格式,就是指定大端字节序还是小端字节序
值为0表示非法编码格式
值为1表示小端字节序,即LSB(最低有效字节)
值为2表示大端字节序,即MSB(最高有效字节)
e_ident[6] ELF版本信息,默认为1
e_ident[7~15] 保留,初始化为0

e_type占用两字节,用来指定elf目标文件的类型

elf目标文件类型 取值 意义
ET_NONE 0 未知目标文件格式
ET_REL 1 可重定位文件
ET_EXEC 2 可执行文件
ET_DYN 3 动态共享目标文件
ET_CORE 4 core文件,即程序崩溃时其内存映像的转储格式,俗称出core了
ET_LOPROC 0xff00 特定处理器文件的扩展下边界
ET_HIPROC 0xffff 特定处理器文件的扩展上边界

这里列出了许多类型文件,前5种类型文件看上去稍显“亲民”还能接受。最后两种,ET_LOPROC和ET_HIPROC这两个类型的取值跨度好大,显得似乎有些怪异,其实把它们搞得如此怪异,是为了突显它们的“与众不同”,它们是与硬件相关的参数,在它们之间的取值用来标识与处理器相关的文件格式。不过呢,虽然有这么多类型,但咱们只需要关注取值为2的ET_EXEC类型,它的意义为程序可执行,就是咱们平时编译链接好的可执行程序的类型。

e_machine占用2字节,用来描述elf目标文件的体系结构类型,也就是说该文件要在哪种硬件平台(哪种机器)上才能运行。

体系结构类型 取值 意义
EM_NONE 0 未指定
EM_M32 1 AT&T WE 32100
EM_SPARC 2 SPARC
EM_386 3 Intel 80386
EM_68K 4 Motorola 68000
EM_88K 5 Motorola 88000
EM_860 7 Intel 80860
EM_MIPS 8 MIPS RS3000

e_version占用4字节,用来表示版本信息。

e_entry占用4字节,用来指明操作系统运行该程序时,将控制权转交到的虚拟地址。

e_phoff占用4字节,用来指明程序头表(program header table)在文件内的字节偏移量。如果没有程序头表,该值为0。

e_shoff占用4字节,用来指明节头表(section header table)在文件内的字节偏移量。若没有节头表,该值为0。

e_flags占用4字节,用来指明与处理器相关的标志。

e_ehsize占用2字节,用来指明elf header的字节大小。

e_phentsize占用2字节,用来指明程序头表(program header table)中每个条目(entry)的字节大小,即每个用来描述段信息的数据结构的字节大小,该结构是后面要介绍的struct Elf32_Phdr。

e_phnum占用2字节,用来指明程序头表中条目的数量。实际上就是段的个数。

e_shentsize占用2字节,用来指明节头表(section header table)中每个条目(entry)的字节大小,即每个用来描述节信息的数据结构的字节大小。

e_shnum占用2字节,用来指明节头表中条目的数量。实际上就是节的个数。

e_shstrndx占用2字节,用来指明string name table在节头表中的索引index。

Elf32_Phdr

此段是指程序中的某个数据或代码的区域段落,例如数据段或代码段,并不是内存中的段,到现在为止我们都在讨论位于磁盘上的程序文件呢

段描述符用来描述物理内存中的一个内存段,而struct Elf32_Phdr是用来描述位于磁盘上的程序中的一个段,它被加载到内存后才属于GDT中段描述符所指向的内存段的子集

typedef struct
{
    
    
  Elf32_Word    p_type;         /* Segment type */
  Elf32_Off     p_offset;       /* Segment file offset */
  Elf32_Addr    p_vaddr;        /* Segment virtual address */
  Elf32_Addr    p_paddr;        /* Segment physical address */
  Elf32_Word    p_filesz;       /* Segment size in file */
  Elf32_Word    p_memsz;        /* Segment size in memory */
  Elf32_Word    p_flags;        /* Segment flags */
  Elf32_Word    p_align;        /* Segment alignment */
} Elf32_Phdr;

p_type占用4字节,用来指明程序中该段的类型,p_type取值见下表

类型 取值 意义
PT_NULL 0 忽略
PT_LOAD 1 可加载程序段
PT_DYNAMIC 2 动态链接信息
PT_INTERP 3 动态加载器名称
PT_NOTE 4 一些辅助的附加信息
PT_SHLIB 5 保留
PT_PHDR 6 程序头表
PT_LOPROC 0x70000000 此范围内的类型预留给处理器专用
PT_HIPROC 0x7fffffff 此范围内的类型预留给处理器专用

p_offset占用4字节,用来指明本段在文件内的起始偏移字节。

p_vaddr占用4字节,用来指明本段在内存中的起始虚拟地址。

p_paddr占用4字节,仅用于与物理地址相关的系统中,因为System V忽略用户程序中所有的物理地址,所以此项暂且保留,未设定。

p_filesz占用4字节,用来指明本段在文件中的大小。

p_memsz占用4字节,用来指明本段在内存中的大小。

p_flags占用4字节,用来指明与本段相关的标志,此标志取值范围见表

类型 取值 意义
PF_X 1 本段具有可执行权限
PF_W 2 本段具有可写权限
PF_R 4 本段具有可读权限
PF_MASKOS 0x0ff00000 本段与操作系统相关
PF_MASKPROC 0xf0000000 本段与处理器相关

p_align占用4字节,用来指明本段在文件和内存中的对齐方式。如果值为0或1,则表示不对齐。否则p_align应该是2的幂次数。

链接后,程序运行的代码、数据等资源都是在段中,所以,在elf header和program header介绍完后,基本上就已经把与“段”相关的内容说完啦。咱们还是本着“够用就行”的原则学习,有关“section节”或其他方面的内容咱们这里就不需要关注太多了,有兴趣的同学可以自行深入研究。

实例

记得我们上面那个mian.c嘛,我们来分析分析他

编译

gcc -c -no-pie -fno-pic -fcf-protection=none -mmanual-endbr -m32 -o main.o main.c

链接

ld main.o -Ttext 0xc0001500 -e main -o Kernel.bin -m elf_i386

readelf分析

lovetzp@ubuntu:~/os/kernel$ readelf -a Kernel.bin 
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Intel 80386
  Version:                           0x1
  Entry point address:               0xc0001500
  Start of program headers:          52 (bytes into file)
  Start of section headers:          4428 (bytes into file)
  Flags:                             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         4
  Size of section headers:           40 (bytes)
  Number of section headers:         7
  Section header string table index: 6

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .text             PROGBITS        c0001500 000500 000005 00  AX  0   0  1
  [ 2] .eh_frame         PROGBITS        c0002000 001000 000034 00   A  0   0  4
  [ 3] .comment          PROGBITS        00000000 001034 00002b 01  MS  0   0  1
  [ 4] .symtab           SYMTAB          00000000 001060 000090 10      5   5  4
  [ 5] .strtab           STRTAB          00000000 0010f0 000025 00      0   0  1
  [ 6] .shstrtab         STRTAB          00000000 001115 000034 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  p (processor specific)

There are no section groups in this file.

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  LOAD           0x000000 0xc0001000 0xc0000000 0x000b4 0x000b4 R   0x1000
  LOAD           0x000500 0xc0001500 0xc0001500 0x00005 0x00005 R E 0x1000
  LOAD           0x001000 0xc0002000 0xc0002000 0x00034 0x00034 R   0x1000
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x10

 Section to Segment mapping:
  Segment Sections...
   00     
   01     .text 
   02     .eh_frame 
   03     

There is no dynamic section in this file.

There are no relocations in this file.

The decoding of unwind sections for machine type Intel 80386 is not currently supported.

Symbol table '.symtab' contains 9 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: c0001500     0 SECTION LOCAL  DEFAULT    1 
     2: c0002000     0 SECTION LOCAL  DEFAULT    2 
     3: 00000000     0 SECTION LOCAL  DEFAULT    3 
     4: 00000000     0 FILE    LOCAL  DEFAULT  ABS main.c
     5: c0004000     0 NOTYPE  GLOBAL DEFAULT    2 __bss_start
     6: c0001500     5 FUNC    GLOBAL DEFAULT    1 main
     7: c0004000     0 NOTYPE  GLOBAL DEFAULT    2 _edata
     8: c0004000     0 NOTYPE  GLOBAL DEFAULT    2 _end

No version information found in this file.

可以把这些详细信息都给我们罗列出来

nm分析

lovetzp@ubuntu:~/os/kernel$ nm Kernel.bin 
c0004000 R __bss_start
c0004000 R _edata
c0004000 R _end
c0001500 T main

xdd分析

lovetzp@ubuntu:~/os/kernel$ sh ../tool/xxd.sh Kernel.bin 0 500
00000000: 7F 45 4C 46 01 01 01 00 00 00 00 00 00 00 00 00  .ELF............
00000010: 02 00 03 00 01 00 00 00 00 15 00 C0 34 00 00 00  ............4...
00000020: 4C 11 00 00 00 00 00 00 34 00 20 00 04 00 28 00  L.......4. ...(.
00000030: 07 00 06 00 01 00 00 00 00 00 00 00 00 10 00 C0  ................
00000040: 00 00 00 C0 B4 00 00 00 B4 00 00 00 04 00 00 00  ................
00000050: 00 10 00 00 01 00 00 00 00 05 00 00 00 15 00 C0  ................
00000060: 00 15 00 C0 05 00 00 00 05 00 00 00 05 00 00 00  ................
00000070: 00 10 00 00 01 00 00 00 00 10 00 00 00 20 00 C0  ............. ..
00000080: 00 20 00 C0 34 00 00 00 34 00 00 00 04 00 00 00  . ..4...4.......
00000090: 00 10 00 00 51 E5 74 64 00 00 00 00 00 00 00 00  ....Q.td........
000000a0: 00 00 00 00 00 00 00 00 00 00 00 00 06 00 00 00  ................
000000b0: 10 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
000000c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................

第一行前四个字节为魔术值 第五个字节表示这个文件为32位elf格式,第六个字节表示小端字节序,第七个字节表示版本为1
e_type 0x0002 可执行文件
e_machine 0x0003 Intel 80386
e_version 0x00000001 版本信息
e_entry 0xc0001500 用来指明操作系统运行该程序时,将控制权转交到的虚拟地址。
e_phoff 0x00000034 用来指明程序头表(program header table)在文件内的字节偏移量
e_shoff 0x0000114c 用来指明节头表(section header table)在文件内的字节偏移量
e_flags 0x00000000用来指明与处理器相关的标志
e_ehsize 0x0034 用来指明elf header的字节大小。
e_phentsize 0x0020 用来指明程序头表(program header table)中每个条目(entry)的字节大小
e_phnum 0x0004 用来指明程序头表中条目的数量。实际上就是段的个数。
e_shentsize 0x0028 用来指明节头表(section header table)中每个条目(entry)的字节大小,
e_shnum 0x0007用来指明节头表中条目的数量。实际上就是节的个数。
e_shstrndx 0x0006 用来指明string name table在节头表中的索引index。

p_type 0x00000001 可加载程序段
p_offset 0x00000000用来指明本段在文件内的起始偏移字节
p_vaddr 0xc0001000用来指明本段在内存中的起始虚拟地址
p_paddr 0xc0000000仅用于与物理地址相关的系统中,因为System V忽略用户程序中所有的物理地址,因此为0
p_filesz 0x0000000B4 用来指明本段在文件中的大小
p_memsz 0x0000000B4用来指明本段在内存中的大小
p_flags 0x00000004用来指明与本段相关的标志,具有可读权限
p_align 0x000010004KB对齐

上面的是第一个程序段,这意思是还有程序段?没错,还有

                      01 00 00 00 00 05 00 00 00 15 00 C0  ................
00000060: 00 15 00 C0 05 00 00 00 05 00 00 00 05 00 00 00  ................
00000070: 00 10 00 00 
                      01 00 00 00 00 10 00 00 00 20 00 C0  ............. ..
00000080: 00 20 00 C0 34 00 00 00 34 00 00 00 04 00 00 00  . ..4...4.......
00000090: 00 10 00 00 

这个程序编译出了三个段,所以会有三个这样的结构

猜你喜欢

转载自blog.csdn.net/weixin_43903639/article/details/129834282