如何在Linux中从用户空间中找到变量的物理地址?

时间:2010-03-13 23:01:26

标签: c linux

我想找到在用户空间进程中定义的变量的物理地址?有没有办法使用root权限?

4 个答案:

答案 0 :(得分:17)

正如之前部分回答的那样,普通程序不需要担心物理地址,因为它们在虚拟地址空间中运行并具有所有便利性。此外,并非每个虚拟地址都具有物理地址,可以属于映射文件或交换页面。但是,有时看到这种映射可能会很有趣,即使在用户区也是如此。

为此,Linux内核通过/proc中的一组文件将其映射公开给userland。可以找到文档here。简短摘要:

  1. /proc/$pid/maps提供虚拟地址映射列表以及其他信息,例如映射文件的相应文件。
  2. /proc/$pid/pagemap提供有关每个映射页面的更多信息,包括物理地址(如果存在)。
  3. This website提供了一个C程序,它使用该接口转储所有正在运行的进程的映射,并解释它的作用。

答案 1 :(得分:15)

#include "stdio.h"
#include "unistd.h"
#include "inttypes.h"

uintptr_t vtop(uintptr_t vaddr) {
    FILE *pagemap;
    intptr_t paddr = 0;
    int offset = (vaddr / sysconf(_SC_PAGESIZE)) * sizeof(uint64_t);
    uint64_t e;

    // https://www.kernel.org/doc/Documentation/vm/pagemap.txt
    if ((pagemap = fopen("/proc/self/pagemap", "r"))) {
        if (lseek(fileno(pagemap), offset, SEEK_SET) == offset) {
            if (fread(&e, sizeof(uint64_t), 1, pagemap)) {
                if (e & (1ULL << 63)) { // page present ?
                    paddr = e & ((1ULL << 54) - 1); // pfn mask
                    paddr = paddr * sysconf(_SC_PAGESIZE);
                    // add offset within page
                    paddr = paddr | (vaddr & (sysconf(_SC_PAGESIZE) - 1));
                }   
            }   
        }   
        fclose(pagemap);
    }   

    return paddr;
}   

答案 2 :(得分:2)

首先,你为什么要这样做?现代VM系统的目的是从应用程序编程器中去除物理内存布局的复杂性。为每个人提供他们自己的统一地址空间,让他们的生活更轻松。

如果您确实想要这样做,那么您几乎肯定需要使用内核模块。以正常方式获取变量的虚拟地址,使用它来索引进程页表并读取您找到的值(帧的物理地址)。然后添加页面偏移量以获取完整的物理地址。请注意,在启用分页时,您将无法使用此地址。

(如果幸运的话,您可以从/ proc文件系统获取VM区域的帧地址,因此不需要编写内核模块。)

答案 3 :(得分:-5)

(编辑:如果用“物理地址”表示“存储RAM模块的位数”,则以下答案不合适。)

您不需要root权限即可执行此操作。你需要的是一个调试器。我们走了(在x86_64上使用Linux系统):

首先我们需要一个小程序来玩。这个访问全局变量并连续两次打印。它有两个全局变量,我们稍后会在内存中找到它。

#include <stdio.h>

int a, b = 0;

int main(void)
{
    printf("a: ");
    if (fscanf("%d", &a) < 1)
        return 0;

    printf("a = %d\n", myglobal);

    printf("b: ");
    if (fscanf("%d", &b) < 1)
        return 0;

    printf("a = %d, b = %d\n", a, b);

    return 0;
}

步骤1:编译程序并从中删除所有调试信息,因此我们不会从调试器中获得任何我们在现实生活中无法获得的提示。

$ gcc -s -W -Wall -Os -o ab ab.c

步骤2:运行程序并输入两个数字中的一个。

$ ./ab
a: 123
a = 123
b: _

第3步:找到流程。

$ ps aux | grep ab
roland   21601  0.0  0.0   3648   456 pts/11   S+   15:17   0:00 ./ab
roland   21665  0.0  0.0   5132   672 pts/12   S+   15:18   0:00 grep ab

步骤4:将调试器附加到进程(21601)。

$ gdb
...
(gdb) attach 21601
...
(gdb) where
#0  0x00007fdecfdd2970 in read () from /lib/libc.so.6
#1  0x00007fdecfd80b40 in _IO_file_underflow () from /lib/libc.so.6
#2  0x00007fdecfd8230e in _IO_default_uflow () from /lib/libc.so.6
#3  0x00007fdecfd66903 in _IO_vfscanf () from /lib/libc.so.6
#4  0x00007fdecfd7245c in scanf () from /lib/libc.so.6
#5  0x0000000000400570 in ?? ()
#6  0x00007fdecfd2f1a6 in __libc_start_main () from /lib/libc.so.6
#7  0x0000000000400459 in ?? ()
#8  0x00007fffd827da48 in ?? ()
#9  0x000000000000001c in ?? ()
#10 0x0000000000000001 in ?? ()
#11 0x00007fffd827f9a2 in ?? ()
#12 0x0000000000000000 in ?? ()

有趣的帧是5号,因为它位于调用main函数和scanf函数的某些代码之间,所以它必须是我们的main函数。继续调试会话:

(gdb) frame 5
...
(gdb) disassemble $pc $pc+50
...
0x0000000000400570 :     test   %eax,%eax
0x0000000000400572 :     jle    0x40058c <scanf@plt+372>
0x0000000000400574 :     mov    0x2003fe(%rip),%edx        # 0x600978 <scanf@plt+2098528>
0x000000000040057a :     mov    0x2003fc(%rip),%esi        # 0x60097c <scanf@plt+2098532>
0x0000000000400580 :     mov    $0x40068f,%edi
0x0000000000400585 :     xor    %eax,%eax
0x0000000000400587 :     callq  0x4003f8 <printf@plt>
...

现在我们知道函数printf将获得三个参数,其中两个参数只有四个字节。这是一个很好的信号,表明这两个变量是我们的变量ab。所以a的地址是0x600978或0x60097c。让我们通过尝试找出答案:

(gdb) x/w 0x60097c        
0x60097c <scanf@plt+2098532>:   0x0000007b
(gdb) x/w 0x600978
0x600978 <scanf@plt+2098528>:   0x00000000

所以a,首先读取的变量位于地址0x60097c(因为0x0000007b是我们输入的123的十六进制表示),b位于0x600978。

仍然在调试器中,我们现在可以修改变量a,然后继续该程序。

(gdb) set *(int *)0x60097c = 1234567
(gdb) continue

回到要求我们输入两个数字的程序中:

$ ./ab
a: 123
a = 123
b: 5
a = 1234567, b = 5
$