读 <<程序员的自我修改--链接,装载与库>> 和 <<深入理解java虚拟机>>
前阵子复习了一下 final , 然后发现 final 有一个知识点和 JMM 有关 ,然后又想起了 JVM 相关的知识有点模糊 ,然后我又想起了之前看过一部分的一本书 . <<程序员的自我修改>>
程序的生成过程
多个模块链接形成一个可执行文件,才可以执行
一个典型程序的转换处理过程
可以看到我们即将学的链接
在最后的一个步骤 ,
- 前面 : 汇编生成的
可重定位目标程序
- 后面 : 链接生成的可执行目标程序(例如 window 下的
.exe文件
)
下面解释预处理,编译,汇编,链接的几个重要过程。
预处理
编译
汇编
链接
链接操作的步骤
其中步骤2中的合并可以说是归类代码的数据类型,代码和数据分开,最后重写写入符号对应的物理地址。 也就是说链接完成后,物理地址
已经是固定了。
需要说明的是经过链接后的文件实际已经是“0101001”的二进制序列了,只是图为了让我们更加好理解,依然用汇编语言来表示.
第1,2,3步可以统称为合并,第4步为重定位,实际可以总结为两个操作。
需要注意的是第四步中的指令中填入新地址,这个地址指的是虚拟地址。
链接的本质
链接的本质就是合并相同的“节”,例如 : .text 节, .data节,.bss 节,这样形成的一个文件,在liunx 中称之为 EIF 文件,可以说 EIF 文件就是打包好的文件,可以被引入其他的模块中(有点像java 中的 jar ,当需要引入其他模块时,就引入jar包就可以了)
目标文件
目标文件是编译器和汇编器最终生成的产物 , 那么我们先来看一下这种产物
到底是长什么样的
目标文件的格式
目标的格式分为 :
我们链接这本书主要讲的是 linux平台下的 ELF
三类目标文件
- 可重定位目标文件
- 可执行目标文件
- 共享的目标文件
window中
- .exe 是可执行目标文件
- .dll 共享目标文件
ELF 文件表示的两种视图
两种视图代表的是一个东西的不同的表现形式 . (链接视图和执行视图)
我们先来看一下链接视图
链接视图表示的是**可被链接(合并)生成可执行文件或共享目标文件 **
可重定位目标文件格式
趁着上面讲到的链接视图 ,我们接着介绍该视图所对应的目标文件--可重定位目标文件
可以说由三部分组成 :
- ELF header
- 各种节
- section header table
可重定位目标文件--节的信息
这里简单介绍
bss节,就像是java中的
private String str;
它没有实际赋值,只是一个占位符,没有在这个节里的(例子中的 str)分配磁盘空间 。
剩下的节含义
这些需要注意的是每个节都有重要的作用 ,可重定位目标文件中, 特有的重定向节 : .rel.txt , .rel.data ,这两个节非常重要 ,后面我们在重定向的时候会学东西到
可执行目标文件格式
可执行目标文件是可以被载入到内存中去执行的文件,它主要与上面的可重定位文件有什么不同呢?
可执行文件是需要载入到内存中某一个进程中去的,某个进程的虚拟空间有各个段,
程序头表描述了该文件和虚拟地址的映射 (大白话就是我这个文件的哪个节应该映射到哪个虚拟地址)
关于一些重要节和table 的介绍我们留到下一篇文章
目标文件转载到进程的虚拟空间
下图描述的是可执行文件存储映射到某个进程的虚拟空间中,该映射过程主要靠的是ELF头(即程序头表),简单点表述就是各个节映射到进程的指定区域。需要注意的是虚存空间(虚拟地址)是每个进程自己编定的,不是内存的物理地址,这里涉及到我们后面讲到的页表,每个进程它的虚拟地址与真实的内存物理地址有个映射表叫页表,存储在每个进程中。
然后操作系统只会去判断 ,这个页的内容有没有被加载, 要是没有被加载 ,那么就将页对应的虚拟地址的内容加载到页中 (这不就是缺页的处理过程吗)
我们可以看到代码段的地址总是从0x08048000 (虚拟地址)开始的,是只读代码段的首地址。下面看一下一个例子。
上面一段代码,地址从0开始的是没链接前的,而0804*开头则是已经链接过的。
通过例子查看目标文件内容
汇编生成文件
/* 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_var = 85;
static int static_var2;
// 这两个是局部变量
int a = 1;
int b;
func1(static_var + static_var2 + a + b);
return a;
}
gcc -c SimpleSection.c
然后我们得到了
[root@k8s-master c_compile_test]# ls
SimpleSection.c SimpleSection.o
我用了 winhex 这个工具看了 SimpleSection.o
这个文件的文件内容, 如图所示 :
打印各个段的信息
[root@k8s-master c_compile_test]# objdump -h SimpleSection.o
SimpleSection.o: 文件格式 elf64-x86-64
节:
Idx Name Size VMA LMA File off Algn
0 .text 00000059 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000008 0000000000000000 0000000000000000 0000009c 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000004 0000000000000000 0000000000000000 000000a4 2**2
ALLOC
3 .rodata 0000000a 0000000000000000 0000000000000000 000000a4 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .comment 0000002e 0000000000000000 0000000000000000 000000ae 2**0
CONTENTS, READONLY
5 .note.GNU-stack 00000000 0000000000000000 0000000000000000 000000dc 2**0
CONTENTS, READONLY
6 .eh_frame 00000058 0000000000000000 0000000000000000 000000e0 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
VMA
虚拟内存区域是空的 ,因为这是可重定向目标文件 ,所以是空的, 也就是说目前还是半成品, 是不会分配虚拟地址的, 只有经过链接以后才会生成虚拟地址空间
查看每个段的大小
[root@k8s-master c_compile_test]# size SimpleSection.o
text data bss dec hex filename
187 8 4 199 c7 SimpleSection.o
把各段内容用 16进制打印出来 , -d
则是反汇编
[root@k8s-master c_compile_test]# objdump -s -d SimpleSection.o
SimpleSection.o: 文件格式 elf64-x86-64
Contents of section .text:
0000 554889e5 4883ec10 897dfc8b 45fc89c6 UH..H....}..E...
0010 bf000000 00b80000 0000e800 000000c9 ................
0020 c3554889 e54883ec 10c745fc 01000000 .UH..H....E.....
0030 8b150000 00008b05 00000000 01c28b45 ...............E
0040 fc01c28b 45f801d0 89c7b800 000000e8 ....E...........
0050 00000000 8b45fcc9 c3 .....E...
Contents of section .data:
0000 54000000 55000000 T...U...
Contents of section .rodata:
0000 68656c6c 6f202564 0a00 hello %d..
Contents of section .comment:
0000 00474343 3a202847 4e552920 342e382e .GCC: (GNU) 4.8.
0010 35203230 31353036 32332028 52656420 5 20150623 (Red
0020 48617420 342e382e 352d3434 2900 Hat 4.8.5-44).
Contents of section .eh_frame:
0000 14000000 00000000 017a5200 01781001 .........zR..x..
0010 1b0c0708 90010000 1c000000 1c000000 ................
0020 00000000 21000000 00410e10 8602430d ....!....A....C.
0030 065c0c07 08000000 1c000000 3c000000 .\..........<...
0040 00000000 38000000 00410e10 8602430d ....8....A....C.
0050 06730c07 08000000 .s......
Disassembly of section .text:
0000000000000000 <func>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: 89 7d fc mov %edi,-0x4(%rbp)
b: 8b 45 fc mov -0x4(%rbp),%eax
e: 89 c6 mov %eax,%esi
10: bf 00 00 00 00 mov $0x0,%edi
15: b8 00 00 00 00 mov $0x0,%eax
1a: e8 00 00 00 00 callq 1f <func+0x1f>
1f: c9 leaveq
20: c3 retq
0000000000000021 <main>:
21: 55 push %rbp
22: 48 89 e5 mov %rsp,%rbp
25: 48 83 ec 10 sub $0x10,%rsp
29: c7 45 fc 01 00 00 00 movl $0x1,-0x4(%rbp)
30: 8b 15 00 00 00 00 mov 0x0(%rip),%edx # 36 <main+0x15>
36: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 3c <main+0x1b>
3c: 01 c2 add %eax,%edx
3e: 8b 45 fc mov -0x4(%rbp),%eax
41: 01 c2 add %eax,%edx
43: 8b 45 f8 mov -0x8(%rbp),%eax
46: 01 d0 add %edx,%eax
48: 89 c7 mov %eax,%edi
4a: b8 00 00 00 00 mov $0x0,%eax
4f: e8 00 00 00 00 callq 54 <main+0x33>
54: 8b 45 fc mov -0x4(%rbp),%eax
57: c9 leaveq
58: c3 retq
其中 .data
段 8个字节,存放着是我们变量 static int static_var = 85
和 int global_init_var = 84
, 然后我们对比这前面的 , .data
段的 offset
是 9c
, 就是这里
链接以后
链接上面例子的文件
使用打印,打印出关于ELF的消息
其他
每次生成可执行目标文件的虚拟地址会不会不同 ??
我们做一下实验就知道了, 上面的地址是 :
然后我们删除以后, 在链接生成一个新的可执行目标文件,并查看其虚拟地址
我们看这两个需要加载进虚拟空间的内存的虚拟地址 , 前后两次是一样的
参考
- <<程序员的自我修改--链接,装载与库>> 书