为什么我的Rust可执行文件被映射到如此高的地址(靠近堆栈),而不是0x400000?

7

我正在学习关于x86_64系统上Linux用户空间内存布局,并希望打印一些部分的地址。我使用了以下Rust代码:

fn main() {
    let x = 3;        // should be stored on stack
    let s = "hello";  // should be in the .data section

    println!("stack ≈ {:p}", &x);
    println!(".text ≈ {:p}", main as *const ());
    println!(".data ≈ {:p}", s);


    use std::io;

    let mut f = std::fs::File::open("/proc/self/maps").unwrap();
    let out = io::stdout();
    io::copy(&mut f, &mut out.lock()).unwrap();
}

这段代码还会将文件/proc/self/maps打印到标准输出。我只是使用rustc mem.rs编译了这个名为mem.rs的文件。它输出了:

stack ≈ 0x7ffffbf82f2c
.text ≈ 0x7f45b7c0a2b0
.data ≈ 0x7f45b7c4d35b

7f45b6800000-7f45b6c00000 rw-- 00000000 00:00 0
7f45b6de0000-7f45b6f9a000 r-x- 00000000 00:00 664435             /lib/x86_64-linux-gnu/libc-2.19.so
7f45b6f9a000-7f45b6fa2000 ---- 001ba000 00:00 664435             /lib/x86_64-linux-gnu/libc-2.19.so
[ ... more .so files]
7f45b7a22000-7f45b7a23000 r--- 00022000 00:00 663920             /lib/x86_64-linux-gnu/ld-2.19.so
7f45b7a23000-7f45b7a24000 rw-- 00023000 00:00 663920             /lib/x86_64-linux-gnu/ld-2.19.so
7f45b7a24000-7f45b7a25000 rw-- 00000000 00:00 0
7f45b7aa0000-7f45b7aa2000 rw-- 00000000 00:00 0
7f45b7ab0000-7f45b7ab2000 rw-- 00000000 00:00 0
7f45b7ac0000-7f45b7ac1000 rw-- 00000000 00:00 0
7f45b7ad0000-7f45b7ad1000 rw-- 00000000 00:00 0
7f45b7ae0000-7f45b7ae2000 rw-- 00000000 00:00 0
7f45b7c00000-7f45b7c5f000 r-x- 00000000 00:00 1134580            /home/lukas/tmp/mem
7f45b7e5e000-7f45b7e62000 r--- 0005e000 00:00 1134580            /home/lukas/tmp/mem
7f45b7e62000-7f45b7e63000 rw-- 00062000 00:00 1134580            /home/lukas/tmp/mem
7f45b7e63000-7f45b7e64000 rw-- 00000000 00:00 0
7ffffb784000-7ffffb785000 ---- 00000000 00:00 0                  [stack]
7ffffb785000-7ffffbf84000 rw-- 00000000 00:00 0
7ffffc263000-7ffffc264000 r-x- 00000000 00:00 0                  [vdso]

至少我自己打印的地址似乎与“地图”所说的匹配。但是当我在终端中执行cat /proc/self/maps时,会得到以下输出:

00400000-0040b000 r-x- 00000000 00:00 107117                     /bin/cat
0060a000-0060b000 r--- 0000a000 00:00 107117                     /bin/cat
0060b000-0060c000 rw-- 0000b000 00:00 107117                     /bin/cat
0071c000-0073d000 rw-- 00000000 00:00 0                          [heap]
7f7deb933000-7f7debc30000 r--- 00000000 00:00 758714             /usr/lib/locale/locale-archive
7f7debc30000-7f7debdea000 r-x- 00000000 00:00 664435             /lib/x86_64-linux-gnu/libc-2.19.so
7f7debdea000-7f7debdf2000 ---- 001ba000 00:00 664435             /lib/x86_64-linux-gnu/libc-2.19.so
[ ... more .so files ...]
7f7dec222000-7f7dec223000 r--- 00022000 00:00 663920             /lib/x86_64-linux-gnu/ld-2.19.so
7f7dec223000-7f7dec224000 rw-- 00023000 00:00 663920             /lib/x86_64-linux-gnu/ld-2.19.so
7f7dec224000-7f7dec225000 rw-- 00000000 00:00 0
7f7dec250000-7f7dec252000 rw-- 00000000 00:00 0
7f7dec260000-7f7dec261000 rw-- 00000000 00:00 0
7f7dec270000-7f7dec272000 rw-- 00000000 00:00 0
7ffff09e8000-7ffff11e8000 rw-- 00000000 00:00 0                  [stack]
7ffff1689000-7ffff168a000 r-x- 00000000 00:00 0                  [vdso]

后面的结果与我所读到的有关此主题的所有内容都匹配:可执行文件的部分被映射在虚拟地址空间的较低端(大约从0x400000开始)。

我在Windows的Linux子系统(基本上是Ubuntu 14.04)中执行和编译了所有内容。我知道,这很新颖,但我相当确定这不是子系统的问题(如果是,请告诉我!)。Rust 1.14很重要(我怀疑),

我还尝试了一个C程序(请原谅我的可能不好的C语言):

#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>

int main(int argc, char **argv) {
    FILE *test_file;
    char buf[4096];
    if ((test_file = fopen ("/proc/self/maps", "r")) != NULL) {
        while (!feof (test_file)) {
            fgets (buf, sizeof (buf), test_file);
            puts (buf);
        }
    }

    return 0;
}

它会输出类似于cat的内容:
00400000-00401000 r-x- 00000000 00:00 1325490                    /home/lukas/tmp/a.out
00600000-00601000 r--- 00000000 00:00 1325490                    /home/lukas/tmp/a.out
00601000-00602000 rw-- 00001000 00:00 1325490                    /home/lukas/tmp/a.out

为什么Rust可执行文件在堆栈附近被映射到大地址?

2
说实话,将你的Rust代码运行于Ubuntu 16.04 Docker镜像中,我得到的结果是:stack ≈ 0x7ffc2a01636c.text ≈ 0x559f58ae4270.data ≈ 0x559f58b2735b。 这与栈并不接近。 - Shepmaster
1个回答

9
使用 rustc -Z print-link-args addr.rs 命令,您可以查看 Rust 编译器将使用的链接器调用。由于当前链接器恰好是 cc,因此我们可以直接将这些选项重用于 C 程序。忽略不重要的参数并逐个删除其他参数,我得到了以下编译器调用:
gcc -fPIC -pie addr.c -o addr-c

这样编译C代码会产生与Rust编译的可执行文件类似的地址,表明其中一个或两个选项可能是罪魁祸首。这就将问题转变为“为什么-fPIC和/或-pie会映射到如此高的地址?”
我找到了另一个问题和答案, 看起来可以解释这个问题:

PIE二进制文件的链接方式就像共享库一样,因此它的默认加载地址(第一个LOAD段的.p_vaddr)为零。期望的是,某些东西将此二进制文件从零页重新定位,并将其加载到某个随机地址。

使用readelf -e命令查看Rust可执行文件,我们可以看到第一个LOAD段确实具有虚拟地址为零:
Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x000000000005e6b4 0x000000000005e6b4  R E    200000
  LOAD           0x000000000005ead0 0x000000000025ead0 0x000000000025ead0
                 0x00000000000039d1 0x00000000000049e8  RW     200000

我猜这就把问题变成了“为什么选择这些随机地址”,但我不确定这个答案。 ^_^ 直觉告诉我ASLR起到了作用。这个其他答案似乎证实了这一点:

PIE支持可执行文件的ASLR

ASLR是一种安全技术,有助于加强程序对某些类型攻击的防御,因此Rust采用这种安全思路,尝试默认启用类似的技术。确实,每次调用时地址会稍微改变一点:
root@97bcff9a925c:/# ./addr | grep 'r-xp' | grep 'addr'
5587cea9d000-5587ceafc000 r-xp 00000000 00:21 206                        /addr
561d8aae2000-561d8ab41000 r-xp 00000000 00:21 206                        /addr
555c30ffd000-555c3105c000 r-xp 00000000 00:21 206                        /addr
55db249d5000-55db24a34000 r-xp 00000000 00:21 206                        /addr
55e988572000-55e9885d1000 r-xp 00000000 00:21 206                        /addr
560400e1b000-560400e7a000 r-xp 00000000 00:21 206                        /addr

ASLR不会在每次执行时给出不同的地址吗? - Matthieu M.
@MatthieuM。我一直在思考ASLR的范围。每次我都会得到略微不同的地址,但在我的机器上它总是以0x7f开头(从未像Shepmaster上面的评论中那样以0x55开头)。无论如何,很棒的回答,Shepmaster! - Lukas Kalbertodt
@MatthieuM。我每次执行时都会得到不同的地址,我已经更新了它。 - Shepmaster
@Shepmaster:太好了,那肯定是地址空间布局随机化(ASLR)。 - Matthieu M.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接