问题
今天调试代码的时候看到地址的时候突然感到奇怪:我记得我之前看到的代码地址空间好多都是 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 才可以使用。这样固定的情况也比较好调试,因为虚拟地址固定。