文章目录
- 逻辑地址
- 进程地址空间
- 指令的执行
- 惰性加载(Lazy Loading)/延迟加载
- 动态库的地址
逻辑地址
在程序加载进内存之前,即编译之后就已经形成了地址,在编译之后的地址被称为 逻辑地址;
-
逻辑地址
逻辑地址是程序在编译时产生的地址;
在编译阶段,编译器会为程序中的每个变量函数等元素生成一个相对的地址即逻辑地址;
这些地址是相对于程序开始的位置而言,他们不是物理内存地址;
假设一个源文件(main.c
)中内容为:
#include <stdio.h>
int main()
{
printf("hello world\n");
int x = 10;
int y = 20;
int z = x + y;
printf("z = x + y = %d\n", z);
return 0;
}
使用gcc
将源文件进行编译链接形成可执行程序a.out
;
gcc main.c
利用objdump
工具选择-S
选项可以观察可执行程序的反汇编情况,在此处即objdump -S a.out
;
$ objdump -S a.out
a.out: file format elf64-x86-64
Disassembly of section .init:
0000000000400418 <_init>:
400418: 48 83 ec 08 sub $0x8,%rsp
40041c: 48 8b 05 d5 0b 20 00 mov 0x200bd5(%rip),%rax # 600ff8 <__gmon_start__>
400423: 48 85 c0 test %rax,%rax
400426: 74 05 je 40042d <_init+0x15>
400428: e8 53 00 00 00 callq 400480 <__gmon_start__@plt>
40042d: 48 83 c4 08 add $0x8,%rsp
400431: c3 retq
# ......
# 略....
# ......
0000000000400620 <__libc_csu_fini>:
400620: f3 c3 repz retq
Disassembly of section .fini:
0000000000400624 <_fini>:
400624: 48 83 ec 08 sub $0x8,%rsp
400628: 48 83 c4 08 add $0x8,%rsp
40062c: c3 retq
其中代码中400418
至40062c
即为编译后所生成的逻辑地址;
源文件进行编译后生成可执行程序后将存在逻辑地址;
逻辑地址一般按照段进行分布,意思就是当编译过后最终的指令地址将以不同的段落进行分组;
每个分组被称为段描述,主要的段包括:
-
.text
段包含编译后的机器指令,即程序代码;
-
.data
段包含已初始化的全局变量和静态变量;
-
.rodata\.rdonly
段包含只读数据,例如字符串常量等;
-
.bss
段用于未初始化的全局变量和静态变量;
一般情况下该段不占用实际的文件空间,但是会被标记在程序运行时需要分配的空间;
-
.symtab
段(符号表),.strtab
段(字符串表),.debug
段包含用于调试和符号解析的信息;
在使用objdump -S
时主要为用户展示主要为.text
段与源代码(如果可用)中的内容;
若是需要查看整体的逻辑地址段描述的细节可以使用-h
选项:
$ objdump -h a.out
a.out: file format elf64-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
0 .interp 0000001c 0000000000400238 0000000000400238 00000238 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
1 .note.ABI-tag 00000020 0000000000400254 0000000000400254 00000254 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .note.gnu.build-id 00000024 0000000000400274 0000000000400274 00000274 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
3 .gnu.hash 0000001c 0000000000400298 0000000000400298 00000298 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
CONTENTS, ALLOC, LOAD, READONLY, DATA
#...
19 .dynamic 000001d0 0000000000600e28 0000000000600e28 00000e28 2**3
CONTENTS, ALLOC, LOAD, DATA
20 .got 00000008 0000000000600ff8 0000000000600ff8 00000ff8 2**3
CONTENTS, ALLOC, LOAD, DATA
21 .got.plt 00000038 0000000000601000 0000000000601000 00001000 2**3
CONTENTS, ALLOC, LOAD, DATA
22 .data 00000004 0000000000601038 0000000000601038 00001038 2**0
CONTENTS, ALLOC, LOAD, DATA
23 .bss 00000004 000000000060103c 000000000060103c 0000103c 2**0
ALLOC
24 .comment 00000059 0000000000000000 0000000000000000 0000103c 2**0
CONTENTS, READONLY
由于编译器并不知道程序将被加载到内存的具体的哪个位置,因此实际上是一种相对地址;
这些地址基于程序的起始点或某个特定段的起始点;
进程地址空间
程序在加载进内存后会成为进程,操作系统会为进程维护一个进程地址空间(虚拟内存)从而保持物理内存的安全性;
进程地址空间一般是由逻辑地址直接加载进内存的;
当逻辑地址被加载进内存时将会进行一个至进程地址空间的一个转换;
最终通过内存管理单元MMU
与页表的映射机制映射至物理内存;
指令的执行
被编译生成的可执行程序在执行时将会加载进内存;
实际上加载进内存的即为可执行程序的代码和数据;
代码即为编译生成的各个指令集以及其虚拟地址的映射;
所以实际上指令集中的每个指令都将存在其对应的物理地址;
在CPU
中存在一个寄存器为EIP
即pc
指针(指令寄存器);
当可执行程序被加载进内存成为进程时,操作系统将会给这个寄存器中存放这个进程代码的初始位置(虚拟地址 ),例如程序的入口点;
CPU
将会根据EIP
中指向的地址从进程的虚拟地址空间通过页表映射读取指令,并让EIP
读取下一条指令的地址;
实际上EIP
所读取到的指令地址都是虚拟地址;
在整个内存管理系统中,真正代表虚拟地址到物理地址映射的只能通过内存管理单元MMU
与页表之间的映射;
以该图为例为以下几点:
-
加载可执行程序
可执行程序(如
1.exe
)从磁盘中加载进物理内存;加载过程中虚拟地址被分配给程序的各个部分(如代码段,数据段等);
-
虚拟地址与物理地址的映射
程序的虚拟地址包括代码段,已初始化数据段,堆,共享区等;
页表负责将这些虚拟地址映射到物理地址;
-
指令执行过程
当进程被调度执行时,
CPU
中的EIP
(指令寄存器)会指向进程代码的入口地址(虚拟地址);EIP
寄存器中的地址通过页表映射到物理地址从而读取指令;CPU
根据EIP
指向的地址从进程的虚拟地址空间中读取指令并执行指令; -
内存管理单元(
MMU
)MMU
在这个过程中起到关键作用,负责将虚拟地址转化为物理地址;MMU
通过查询页表来完成地之间的转换;
惰性加载(Lazy Loading)/延迟加载
一般情况下一个可执行程序执行成为进程后操作系统将会优先为其维护对应的task_struct
,进程地址空间,页表等结构体,但不一定会第一时间将所有的代码数据都加载进物理内存当中;
这是一种按需加载的策略;
当存运行过程中遇到了未被加载进物理内存的代码和数据时内存管理单元MMU
将会触发缺页中断异常;
当这个异常被操作系统检测到时将会从磁盘中找到对应的代码数据将其加载进内存当中;
通过这种方式,操作系统可以有效管理有限的物理内存资源从而确保那些确实需要被立即访问的数据代码有限被加载进物理内存而不必一开始就分配所有资源从而提高系统的整体性能和响应速度;
动态库的地址
动态库是一个当一个可执行程序依赖时需要外部链接的库;
与静态库不同,在进行链接时静态库的代码数据将直接以拷贝的形式使得其能够与可执行程序融为一体;
而动态库则是当加载器将其加载到物理内存中就只会独一份并且可以被其他依赖该动态库的进程共享;
所以动态库在加载的过程中是不能确定其具体加载的位置的;
在使用gcc/g++
生成动态库时需要使用选项gcc -fPIC
,这个选项是生成一个与位置无关码;
-
位置无关码(
PIC
)这是一种特殊类型的机器码,他可以在内存中的任何位置执行而不需要修改;
这是通过确保代码执行时不依赖它的绝对地址来实现的,即代码对于数据的禁用是基于它当前的运行地址来计算的;
动态库被设计为可以被多个进程共享;
为了使不同的进程能够同时使用同一份物理内存的库副本,库中的代码必须能够运行在任何地址上即它们必须是与位置无关的;
-
内存效率
通过使用位置无关码,操作系统可以更有效的管理内存;
如,它可以避免为每个使用特定库的进程单独加载库的副本从而节省内存资源;
-
动态加载
位置无关码允许动态库在运行时被加载到任意位置使得在链接时提供灵活性;
使用-fPIC
选项所生成的.o
目标文件最终被生成的动态库其中的地址将可以加载到物理内存中的任意位置,并随机为其分配进程地址空间;
假设存在一个动态库函数,编译为位置无关码:
// example.c
int add(int a, int b) {
return a + b;
}
编译这个库为位置无关码:
gcc -shared -fPIC -o libexample.so example.c
编译完成后使用objdump
查看动态库的反汇编:
objdump -d libexample.so
# 简化
00000000000006a0 <add>:
6a0: 55 push %rbp
6a1: 48 89 e5 mov %rsp,%rbp
6a4: 89 7d fc mov %edi,-0x4(%rbp)
6a7: 89 75 f8 mov %esi,-0x8(%rbp)
6aa: 8b 55 fc mov -0x4(%rbp),%edx
6ad: 8b 45 f8 mov -0x8(%rbp),%eax
6b0: 01 d0 add %edx,%eax
6b2: 5d pop %rbp
6b3: c3 retq
在该例子中显示的为函数add
的机器码;
其中使用的是相对寄存器的操作而并没有绝对地址的引用;
动态库中的代码执行时将一句当前的指令指针(EIP
/RIP
)计算相对地址;
即使代码被加载到内存的不同为止也可以使用相对于当前指令的偏移量来访问数据和其他指令;