目标文件和可执行文件(一)

目录

一、目标文件的格式

二、目标文件是什么样的

三、  挖掘SimpleSection.o

​编辑

1.代码段

2.数据段和只读数据段

3. .BSS段   

 4. 其他段

前言

        编译器编译源代码后生成的文件叫做目标文件,目标文件是编译器编译源代码后生成的中间文件,它包含了已经转换成机器代码的程序指令和数据,但是还没有经过最终的链接过程。标文件的结构通常是按照特定的可执行文件格式存储的,这样可以方便后续的链接器将多个目标文件合并成最终的可执行文件。目标文件中可能包含有关代码段(text segment)、数据段(data segment)、只读数据段(read-only data segment)以及符号表等信息。目标文件与最终的可执行文件在结构上的主要区别在于,目标文件还包含一些未解析的符号引用(比如函数调用、变量引用等)。这些符号引用的具体地址和值在链接过程中才会被解析和填充,使得最终的可执行文件能够正确执行。链接部分我们放在后面来讲。

一、目标文件的格式

  • 现在pc端流行的可执行文件的格式主要有windows下的PE和linux下的ELF文件。目标文件是编译器编译源代码后生成的中间文件,但还未经过链接(windos下的.obj和linux下的.o)。
  • 除了可执行文件(如Windows的.exe和Linux下的ELF可执行文件)外,动态链接库(DLL或.so)和静态链接库(LIB或.a)也都按照可执行文件格式存储。在Windows平台,可执行文件、动态链接库和静态链接库都使用PE (Portable Executable) 格式。PE是一种可执行文件格式,它定义了文件的结构和组织方式,使得操作系统和执行器能够理解和加载这些文件,在Linux平台,可执行文件、动态链接库和静态链接库都使用ELF (Executable and Linkable Format) 格式。ELF同样定义了文件的结构和组织方式,用于支持可执行文件和共享库的加载和执行。

         我们可以在Linux下使用file命令米查看相应的文件格式,上面几种文件在fle命令下会显示出相应的类型:

foobar.o:

File Type: ELF 32-bit LSB relocatable
Architecture: Intel 80386
Version: 1 (SYSV)
Stripped: Not stripped

/bin/bash:

File Type: ELF 32-bit LSB executable
Architecture: Intel 80386
Version: 1 (SYSV)
OS: GNU/Linux 2.6.8
Linked Dynamically: Yes (uses shared libs)
Stripped: Yes

/lib/libc-2.6.1.so:

File Type: ELF 32-bit LSB shared object
Architecture: Intel 80386
Version: 1 (SYSV)
OS: GNU/Linux 2.6.8
Stripped: Yes

二、目标文件是什么样的

        目标文件中的内容至少有编译后的机器指令代码、数据。,除了这些内容以外,目标文件中还包括了链接时所须要的一些信息,比如符号表、调试信息、字符串等。一般目标文件将这些信息按不同的属性,以“节”(Section)的形式存储,有时候也叫“段”(Segment)。

在ELF(Executable and Linkable Format)中,段是组织目标文件中数据的基本单位。不同的段存储不同类型的信息,例如:

  1. 代码段(Code Section):这是存放编译后的机器指令代码的地方,也称为文本段(Text Section)。一般使用名为“.code”或“.text”的段名来表示。

  2. 数据段(Data Section):这是存放全局变量和局部静态变量等数据的地方。通常使用名为“.data”的段名来表示。

        除了代码段和数据段之外,目标文件中还可能包含其他类型的段,比如:

  • BSS段(Block Started by Symbol):用于存放未初始化的全局变量和静态变量。这些变量在目标文件中只是声明了,并没有实际的存储空间,它们的实际内存分配是在程序加载时进行的。

  • 符号表(Symbol Table):用于存放符号(函数、变量等)的信息,供链接器在链接阶段解析符号引用。

  • 调试信息(Debug Information):包含用于调试的信息,比如变量名、函数名等的映射关系,用于在调试器中查看源代码和变量值。

  • 字符串表(String Table):存放字符串常量,比如变量名、函数名等。

  • 重定位表(Relocation Table):用于保存需要进行重定位的地址信息,供链接器在最终生成可执行文件时修正地址。

         如图所示即为一个ELF可执行文件的内部结构,可以发现ELF文件的开头是一个文件头(File Header)它描述了整个文件的文件属性,包括文件是否可执行、静态链接还是动态链接及入口地址、目标硬件、目标操作系统等。文件头还包括一个段表(section Table),段表是用来描述文件中的各个段的数组,段表描述了文件中各个段在文件中的偏移位置及段的属性等。从段表可以得到每个段的所有信息,文件头后的就是各段的内容。

        编译后执行语句都被编译为机器代码,保存在.text段,已初始化的全局变量和局部静态变量都保存在.data段内,未初始化的全局变量和局部静态变量保存在.bss段内。未初始化的全局变量和局部静态变量的默认值都为0,所以存在.data段内是没有必要的,但是程序运行时,它们是必须占用内存的,所以可执行文件必须记录所有未初始化的全局变量和局部静态变量的大小总和,记为.bss段,目的只是为了预留位置,其本身没有内容也不占据空间,在程序被加载到内存时,操作系统会为其分配空间。

        程序源代码被编译以后主要分成两种段:程序指令和程序数据。代码段属于程序指令,而数据段和.bss段属于程序数据。主要是因为一下几方面原因:

  1. 一方面是当程序被装载后,数据和指令分别被映射到两个虚存区域。由于数据区域对于进程来说是可读写的,而指令区域对于进程来说是只读的,所以这两个虚存区域的权限可以被分别设置成可读写和只读。这样可以防止程序的指令被有意或无意地改写。
  2. 另外一方面是对于现代的CPU来说,它们有着极为强大的缓存(Cache)体系。由于缓存在现代的计算机中地位非常重要,所以程序必须尽量提高缓存的命中率。指令区和数据区的分离有利于提高程序的局部性。现代CPU的缓存一般都被设计成数据缓存和指令缓存分离,所以程序的指令和数据被分开存放对CPU的缓存命中率提高有好处。CPU的缓存命中率是指在计算过程中,处理器访问缓存中的数据或指令的比例,即缓存命中的次数与总的访问次数之间的比值。缓存是一种高速且小容量的存储器,用于暂存频繁访问的数据或指令,以提高数据访问的效率。L1缓存命中率:L1缓存是位于CPU内部的第一级缓存,它是离处理器核心最近的缓存,速度非常快。L1缓存一般分为L1数据缓存和L1指令缓存,分别用于存储数据和指令。L1缓存命中率是处理器从L1缓存中获取数据或指令的成功比例。L2缓存命中率:L2缓存位于L1缓存之后,通常更大但相对较慢。L2缓存用于存储L1缓存未命中的数据或指令。L2缓存命中率是处理器从L2缓存中获取数据或指令的成功比例。L3缓存命中率:L3缓存位于L2缓存之后,它通常是多核处理器共享的高速缓存,容量更大但速度相对较慢。L3缓存命中率是处理器从L3缓存中获取数据或指令的成功比例。
  3. 第三个原因,其实也是最重要的原因,就是当系统中运行着多个该程序的副本时,它]的指令都是一样的,所以内存中只须要保存一份改程序的指令部分。对于指令这种只读的区域来说是这样,对于其他的只读数据也一样。


三、  挖掘SimpleSection.o

         我们使用以下命令去编译,但不链接(-c 代表不链接)

gcc -c simplesection.c

         我们使用binutils的objdump工具查看我们编译过后.o文件的内部结构。

objdump -h Simplesection.o

         除了.text .data .bass三个段外,我们还可以发现另外有.rodata (只读).comment(注释信息) .note.GNU-stack(堆栈提示)三个段。其中最容易理解的是段的长度(Size)和段所在的位置(File Offset),每个段的第2行中的“CONTENTS”、“ALLOC”等表示段的各种属性,“CONTENTS”表示该段在文件中存在。我们可以看到BSS段没有“CONTENTS”,表示它实际.上在ELF文件中不存在内容。“.note.GNU-stack”段虽然有“CONTENTS”,但它的长度为O,这是个很古怪的段,我们暂且忽略它,认为它在ELF文件中也不存在.那么ELF文件中实际存在的也就是“.text”、“.data”、“.rodata”和“.comment'”这4个段了,

.text 段:

大小(Size):0x5B 字节
VMA(Virtual Memory Address):0x00000000
LMA(Load Memory Address):0x00000000
文件偏移(File off):0x00000034
对齐(Alignment):2
属性(Attributes):CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
.data 段:

大小(Size):0x80 字节
VMA(Virtual Memory Address):0x00000000
LMA(Load Memory Address):0x00000090
文件偏移(File off):2**2 (4) 字节
对齐(Alignment):2
属性(Attributes):CONTENTS, ALLOC, LOAD, DATA
.bss 段:

大小(Size):0x40000000 字节
VMA(Virtual Memory Address):0x00000000
LMA(Load Memory Address):0x00000098
文件偏移(File off):2**2 (4) 字节
对齐(Alignment):2
属性(Attributes):ALLOC
.rodata 段:

大小(Size):0x4000000000000000 字节
VMA(Virtual Memory Address):0x00000098
LMA(Load Memory Address):2**0 (1) 字节
文件偏移(File off):0 字节
对齐(Alignment):2
属性(Attributes):CONTENTS, ALLOC, LOAD, READONLY, DATA
.comment 段:

大小(Size):0x2A00000000 字节
VMA(Virtual Memory Address):0x00000000
LMA(Load Memory Address):0x0000009C
文件偏移(File off):2**0 (1) 字节
对齐(Alignment):2
属性(Attributes):CONTENTS, READONLY
.note.GNU-stack 段:

大小(Size):0 字节
VMA(Virtual Memory Address):0x00000000
LMA(Load Memory Address):0x00000000
文件偏移(File off):2**0 (1) 字节
对齐(Alignment):2
属性(Attributes):CONTENTS, READONLY
其中,VMA 和 LMA 分别表示虚拟内存地址和加载内存地址。属性(Attributes)中的各个标识含义如下:

CONTENTS:表示该段在文件中存在内容。
ALLOC:表示该段在进程的内存中分配了空间。
LOAD:表示该段在可执行文件被加载到内存时将被加载。
RELOC:表示该段包含了重定位信息。
READONLY:表示该段是只读的。
CODE:表示该段包含了可执行代码。
DATA:表示该段包含了已初始化的数据。

下图显示了ELF文件中各段的位置和偏移 

1.代码段

        挖掘各个段的内容,我们还是离不开objdump这个利器。objdump的“-s”参数可以将所有段的内容以十六进制的方式打印出来,“-d”参数可以将所有包含指令的段反汇编。我们将objdump输出中关于代码段的内容提取出来,分析一下关于代码段的内容(省略号表示略去无关内容):

objdump -s -d simplesection.o

        最左面一列是偏移量,中间4列是十六进制内容,最右面一列是.text段的ASCI码形式。对照下面的反汇编结果,可以很明显地看到,.text段里所包含的正是SimpleSection.c里两个函数funcl()和main()的指令。.text段的第一个字节“0x55”就是“func1(0”函数的第一条“push%ebp”指令,而最后一个字节 Oxc3i正是mainO函数的最后··条指令“ret”。

2.数据段和只读数据段

        .data段保存的是那些已经初始化了的全局静态变量和局部静态变量。前面的 SimpleSection..c代码里面一共有两个这样的变量,分别是global_init_varabal与static_var。这两个变量每个4个字节,共刚好8个字节,所以“.dta”这个段的大小为8个字节。 SimpleSection.c里面我们在调用“printf”的时候,用到了一个字符串常量“%dn”,它是一种只读数据,所以它被放到了“.rodata”段,我们可以从输出结果看到“.rodata”这个段的4个字节刚好是这个字符串常量的ASCⅡ字节序,最后以0结尾。

 

3. .BSS段   

   .bss 段存放未初始化的全局变量和局部静态变量。这个段为这些变量预留了空间。然而,特别是在全局未初始化变量的情况下,段的大小可能与变量的实际大小不符。

        这个现象涉及到编译器的实现和语言的不同。有些编译器会将全局未初始化变量存放在目标文件的 .bss 段,而有些则不会实际存放内容,只是预留了一个未定义的“COMMON符号”。具体的内存分配和实际存放通常是在最终链接成可执行文件时进行。因此,在预览单个目标文件时, .bss 段的大小可能只是为了预留空间而较小,并不代表其中所有全局未初始化变量的总大小。

          关于编译器的实现,后续章节会深入介绍“弱符号与强符号”和“COMMON块”,来解释全局未初始化变量在编译和链接过程中的处理方式。从实际使用角度来看,我们可以将 .bss 段简单地视为存放全局未初始化变量的空间。

        需要注意的是,未初始化的局部变量通常不会放入任何段,而是会在运行时动态分配内存。静态变量(比如给全局未初始化变量加上 static 修饰)则会存放在 .bss 段。

 4. 其他段

 这些段的名字都是由“.”作为前缀,表示这些表的名字是系统保留的,应用程序也可以使用一些非系统保留的名字作为段名。

5. 自定义段

        自定义段正常情况下,GCC编译出来的目标文件中,代码会被放到“.text”段,全局变量和静态变量会被放到“.dta”和“.bss”段,正如我们前面所分析的。但是有时候你可能希望变量或某些部分代码能够放到你所指定的段中去,以实现某些特定的功能。比如为了满足某些硬件的内存和/O的地址布局,或者是像Liux操作系统内核中用来完成一些初始化和用户空间复制时出现页错误异常等。GCC提供了一个扩展机制,使得程序员可以指定变量所处的段: _attribute_((section("FOO")))int global 42; attribute((section("BAR")))void foo()我们在全局变量或函数之前加上“_attribute._(section("name")”属性就可以把相应的变量或函数放到以“name”作为段名的段中。

 参考《程序员的自我修养》俞甲子

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值