程序员的自我修养-------目标文件(第三章内容)

1. 目标文件概述

可执行文件
windows: *.exe
Linux: elf

目标文件:
windows: *.obj
Linux: *.o

目标文件链接之后生成可执行文件,格式内容类似。

文件格式:
windows: PE-COFF
Linux: elf

类似的还有动态链接库(linux *.so,win: *.dll) 静态链接库(linux:*.a,win: *.lib)

ELF格式类型:

1. 可重定位文件(relocation file) (*.o *.obj)
2. 可执行文件(executable file) (/bin/下的文件 *.exe)linux下可执行文件没有后缀
3. 共享目标文件(shared object file) (*.so *.dll)
4. 核心转储文件(core dump file) (core dump)

查看elf格式文件类型的方法(见下面的加粗部分)
/bin$ file mv
mv: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=52fcfecc8289ff7667bd5aeb016329c43c10d4a1, stripped


elf和PE都属于COFF格式,最早的UNIX为a.out格式,后来微软在此基础上整理出COFF格式,后来有系统在COFF格式下引入了ELF格式。

----------
2. 目标文件内容
目标文件由ELF文件头和段组成
常见的段有:
*.text(*.code代码段, 存放代码code)
*.data(数据段, 存放全局变量)

*.bss (未定义全局变量段)


bss段:
3. 解刨SimpleSection.o
这里提供下源码,只要保存为SimpleSection.c即可
/* simpleSeciont.c */
int printf( const char* format, ...);
//#include <stdio.h>
int global_init_var = 84;
int global_uninit_var;

void func(int i)
{
  printf("hello %d\n", i);
}

int main(void)
{
  static int static_init_var = 85;
  static int static_uninit_var;
  int a = 1;
  int b;
  func(static_init_var + static_uninit_var +
      a + b);
  return 0;

}
我们使用
gcc -c SimpleSection.c
来生成目标文件SimpleSection.o

再使用
objdump -h SimpleSection.o

来看下结构

SimpleSection.o: file format elf64-x86-64

Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000057 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000008 0000000000000000 0000000000000000 00000098 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000004 0000000000000000 0000000000000000 000000a0 2**2
ALLOC
3 .rodata 0000000a 0000000000000000 0000000000000000 000000a0 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .comment 00000035 0000000000000000 0000000000000000 000000aa 2**0
CONTENTS, READONLY
5 .note.GNU-stack 00000000 0000000000000000 0000000000000000 000000df 2**0
CONTENTS, READONLY
6 .eh_frame 00000058 0000000000000000 0000000000000000 000000e0 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA

-h就是打印各个段的基本信息,
Size就是该段的大小,file off就是该段在文件中的偏移位置
objdump -x SimpleSection.o
这条命令可以更多的打印出各段的基本信息。
根据上面的内容可以画出下面的图:
前面一段未显示的0x00000000~0x00000040 为elf文件头,待会会介绍
.text 是代码段 0x00000040 ~0x00000098 大小为0x57
.data 是数据段 0x00000098 ~0x 000000a0 大小为0x08
.bss 是未初始化全局变量 地址为0x000000a0~ 0x000000a0 注意这里是不占文件的空间的。
.rodata 是只读数据段 0x000000a0~ 0x000000aa 大小0x0a
.comment 是注释段 0x000000aa ~0x000000df 大小为0x35
.note.GNU-stack 0x000000df~ 0x000000df 大小为0
.eh_frame 0x000000e0~ 0x00000132 大小为0x58 这个书中没提到,有可能是调试信息,我们暂且不管他。
这边有一点需要注意的是我截图生成的是x64的CPU生成的文件,以及是用比较新的ubuntu16.04生成的,所以有一些部分跟书上不太一样。
再看下SimpleSection.o里面我们用文本编辑器查看下。

地址

其实这个文件很大,我们暂且看section一部分。
我们从右边的字符串显示来看,其实我们看.comment里面其实放的不是注释段,其实里面放的是该目标文件的编译器版本信息。
我们用命令
objdump -s -d SimpleSection.o


这个可以显示汇编和对应的十六进制码。
1. 代码段:
就是上面的.text
2. 数据段:
就是上面的.data

一共是8个字节,我们可以看到里面内容就是0x54,0x55
至于为啥是0x54在前面,这个跟CPU的大小端有关系,CPU是小端
其实就是84,85:
代码里面显示就是:
int global_init_var = 84;
static int static_init_var = 85;
每个变量4个字节,其实就是存放的全局变量和static静态变量。
还有个数据段.rodata

其实就是printf里面的字符串"hello %d\n"
\n其实算一个字节,这里其实只有9个字节,但是rodata里面有0x0a 个字节,还有个字节就是经常会问到的字符串结尾段了,还有个0x00的结束符\0。
3. .bss段
.bss 段是未初始化的全局变量和局部静态变量,
只要值未未初始化的全局变量都会在.bss段。
例如:
static int x1=0;
static int x2=1;
x1就会被编译器优化在.bss段,而x2不会。
4.其他段可以参考书中所讲的内容:

//==================================================================
介绍完主要的段表大家一定很好奇,其他的段呢,没看到。
我们先用readelf -S SimpleSection.o

看到如上图所示的结构图,其实这个显示的比刚才的多很多,其实这个就是文件中所有的段:
最上面【NR】是标题,
我们关注offset和Size两个参数。
.text .data .bss .rodata .commen .note.GNU-stack .eh_frame
这个几个type类型为PROGBITS 就是代表程序段,所以刚才显示的就是这些主要的段。
刚才用的是objdump -h SimpleSection.o

这边刚刚应该是0x040~0x138(0x0xe0+58)
文件的0x00 ~0x40 是文件的ELF头,这个待会再详细讲解。
这边我们再看下文件的大小,

我们用vim命令打开Simplesection.o
输入命令 :%!xxd
拉到最后一行

显示的恰巧是0x779 的数据长度。
很奇怪,刚才只是0x00~ 0x138 这个长度,
而实际文件由0x779的长度。
说明这边还有很多字节的东西了。

再来看下还有哪些
0x138 ~ 0x2b8 .symtab 大小 0x180
0x2b8~0x328 .strtab 大小0x70
0x328~0x3a0 .rela.text 大小0x78
0x3a0 ~0x3d0 .rela.eh_frame 0x30
0x3d0 ~ 0x431 .shstrtab 0x61
一共13个段, 但是只是到0x431
还有0x779-0x430 = 0x349
还有这么多的字节是干嘛的呢?
这个折腾了我好久,想来想去,应该没东西了呀,会是调试的信息吗?

找了半天,看书也看了几遍,也没找到。
其实真相就在上面这张图里。找到这个之后,发现自己是在骑驴找驴。
其实最后一段存放的就是段表。跟书上不同的是,现在的好像段表是放在最后的部分。
vim /usr/include/elf.h
执行这个命令,找出elf.h文件中的段表结构体:
Elf64_Shdr
typedef struct
{
Elf64_Word sh_name; /* Section name (string tbl index) */
Elf64_Word sh_type; /* Section type */
Elf64_Xword sh_flags; /* Section flags */
Elf64_Addr sh_addr; /* Section virtual addr at execution */
Elf64_Off sh_offset; /* Section file offset */
Elf64_Xword sh_size; /* Section size in bytes */
Elf64_Word sh_link; /* Link to another section */
Elf64_Word sh_info; /* Additional section information */
Elf64_Xword sh_addralign; /* Section alignment */
Elf64_Xword sh_entsize; /* Entry size if section holds table */
} Elf64_Shdr;
这边数一数应该有64个字节,
每个段表的数组都是64个字节,则13个段表差不多832=340
这个跟之前剩下的字节差不多,我们再把每个段都要对齐考虑进去,应该就差不多是整个文件的大小了。
==========================================================
好,看清楚里面总共有多少东西我们现在就来一步一步的理解每个段的含义:
1.ELF 头文件
thomas@ubuntu:~/Documents/06_simplesection$ readelf -h SimpleSection.o
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 1080 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 13
Section header string table index: 10


这边看下书上大概也能看懂啥意思:
下面主要讲下命令中的数据的内容:

e_ident ELF魔数 其中45 4C 46 就是ELF的ascii码, 0x7f就是del控制符,几乎所有的ELF文件都是以这4个字节开头的。接下来是标识ELF文件类型的0X01 是32位,0x02是64位。下面个字节是字节序,大小端,01 小端, 02大端。下面个是主版本号,一般都是0x01 。后面9个字节ELF标准未定义,可用于平台自拓展。
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
文件类型: ET_REL 0x01 可重定位文件,一般为.o文件。
ET_EXEC 0x02 可执行文件
ET_DYN 0X03 共享目标文件, 一般为.so文件。
Type: REL (Relocatable file)
机器类型: 这边有很多平台:这边就不展开了


Machine: Advanced Micro Devices X86-64
ELF 版本号: 还是0x01
Version: 0x1
入口程序地址:可重定位文件没有入口地址
Entry point address: 0x0
program开始地址
Start of program headers: 0 (bytes into file)
段表开始的地址: 0x438 这个刚好是之前分析段的最后地址
Start of section headers: 1080 (bytes into file)
ELF标志位,标识平台的相关属性的
Flags: 0x0
ELF头的本身的大小:
Size of this header: 64 (bytes)
program header的大小
Size of program headers: 0 (bytes)
programe header的数量
Number of program headers: 0
段表描述符的大小就是每个段表有多长
Size of section headers: 64 (bytes)
有多少个段
Number of section headers: 13
段表字符串所在的表序号。其实就是.shstrtab段的index的序号。
Section header string table index: 10
2. 重定位表

这边有个.rela.text 就是重定位表段。
看下是在0x328~0x3a0 .rela.text 大小0x78
后面链接的章节会带到这个重定位表。
3.字符串表
这边字符串表讲的其实不是程序中的,而是ELF文件中的,例如有些段的名称,是用户自己定义的,有些说不定很长,所以ELF文件特定划了一段字符串的区域给这些字符串,而在使用的时候,只需要知道偏移就可以了。
例如段表中的第一个元素sh_name就是name的偏移。
其实代表的意思就是段表段(.shstrtab这个就是存放段表名称string的字符串的段)的偏移的名称。

所有的段名都在.shstrtab这个段里面。
.strtab 还有个程序中的一些符号的名称,例如函数名之类的symbol的字符串,所以我们平时不知道有没有想过,程序的标号是放在哪的,一些函数是不是有个字符串在那里。
这些都放在.strtab里面。
0x2b8~0x328 .strtab 大小0x70

这边我们用nm SimpleSection.o
就可以看到这个段的内容

讲到这里,我们也可以顺便把.symtab介绍下。
这个就是跟段表类似的,描述symbol的东西。
其实上面nm SimpleSection.o 就是读的.symtab 里面的东西,而字符串就是选的.strtab里面的东西。
可以这样说
段表 使用.shstrtab里面的字符串
.symtab 符号表使用的是.strtab里面的字符串。
看下符号表的结构:

第一个st_name就是偏移
st_info 符号类型和绑定信息:
低4位为symbolType 高4位为绑定信息

st_other 这个没用到
st_shndx : 符号所在的段, 表示符号所在段的下标。
但是有些特殊值

st_value: 符号值, 每个符号对应一个值,这个跟符号的类型有关,可能是个绝对值,就是某个全局变量的值,也要有可能是地址偏移。
上面的nm可能信息不全。我们用下面这个命令再详细看下:
readelf -s SimpleSection.o

这里的st_value的值不是很多,我们就看有值的地方
12: global_uinit_var值是4 应该是个地址偏移在第3个段表.data段的偏移为0x4.
15:main函数就是在第1个段表.text 的偏移为0x22
....

这边13个段基本讲的差不多了,其余的有可能是新的功能所包含的.eh_frame .rela.eh_frame
eh_frame是dwarf调试信息的变体(variant)
有可能跟调试相关这边就不深究了。
如果我们需要调试信息,在编译目标文件的时候加个-g选项就可以看到调试信息的段的内容。
再总结下命令吧:
1. 读取ELF头文件的内容:
readelf -h SimpleSection.o
2. 读取段表内容:
显示主要内容:
objdump -h SimpleSection.o
显示所有内容:
readelf -S SimpleSection.o
3. 显示代码符号内容:
只显示主要内容:
nm SimpleSection.o
显示所有内容:
readelf -s SimpleSection.o
objdump -t SimpleSeciton.o
4. 反汇编目标文件
显示主要的代码内容:
objdump -d SimpleSection.o
显示所有内容:
objdump -D SimpleSection.o (用处不大,它会把一些全局变量的值,字符串都反汇编成汇编代码)
5. 显示重定位的部分:
readelf -r SimpleSection.o



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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值