进程代码段地址空间随机加载

问题

今天调试代码的时候看到地址的时候突然感到奇怪:我记得我之前看到的代码地址空间好多都是 0x400xxx 开头的,怎么这次的地址空间是 0x5562b845axxx 呢?是什么导致了这个差异?

我换了地址空间为 0x400xxx 开头的机器,准备了相同的代码,在两台不同的机器上编译:

1
2
3
4
#include <stdio.h>
int main() {
printf("%p\n", main);
}

这个简单的程序可以打出 main 函数的地址。经测试,在不同的机上打出的结果有很大差异。

1
2
3
4
5
@└────> # ./a.out 
0x5562b845a649

@└────> # ./b.out
0x400596

答案

经查阅资料,这个问题是 Linux 的 ASLR (Address Space Layout Randomization)导致的。这项技术会在装载时,装载到随机地址,防止黑客利用固定地址注入恶意代码。对于 b.out,没有使用该技术。所以 b.out 的代码段虚拟地址一直是 0x400000 开头。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@└────> # readelf -h b.out 
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: EXEC (Executable file) // 这里是 EXEC
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x4004b0 // 这里是 _start 的绝对地址
Start of program headers: 64 (bytes into file)
Start of section headers: 15608 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 9
Size of section headers: 64 (bytes)
Number of section headers: 30
Section header string table index: 29

可以看到,对于 b.out,他的文件类型是 Executable file,_start 的地址是 0x400xxx 开头。这种就是没有使用 ASLR 技术的。而对于 a.out,结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@└────> # readelf -h a.out 
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: DYN (Shared object file) // 这里是 DYN
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x560 // 这里是 _start 的相对地址
Start of program headers: 64 (bytes into file)
Start of section headers: 12744 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 9
Size of section headers: 64 (bytes)
Number of section headers: 31
Section header string table index: 30

对于 a.out,文件类型为 Shared object file,而且 _start 的地址是个相对地址。就是这个导致的这个差异。每次装载 a.out 时,代码会被加载到随机的位置。可以看到,每次运行,得到的地址都不同。

1
2
3
4
5
6
@└────> # ./a.out 
0x559536d9d649
@└────> # ./a.out
0x559a7a6df649
@└────> # ./a.out
0x55ca5dbd4649

发生根因

之所以发生这个原因,是因为操作系统版本导致的。低版本操作系统默认不使用 ASLR。想要在不同的操作系统上复现这两个方式也很简单:

1
@└────> # gcc 1.c -fPIC -pie

这种方式编译出来的就是使用了 ASLR 技术的。其中 -pie 的意思是 position-independent executable,位置无关的可执行文件。编译时还需要加上 -fPIC (Position-Independent Code)生成位置无关代码。而

1
@└────> # gcc 1.c -no-pie

方式编出来的就是固定地址。有些工具必须使用 -no-pie 才可以使用。这样固定的情况也比较好调试,因为虚拟地址固定。