C语言 如何计算进程内存的虚拟地址?



我正在编写以下程序来检查进程内存布局:

#include <stdio.h>
#include <string.h>
#include <sys/resource.h>
#include <sys/time.h>
#include <unistd.h>
#define CHAR_LEN 255
char filepath[CHAR_LEN];
char line[CHAR_LEN];
char address[CHAR_LEN];
char perms[CHAR_LEN];
char offset[CHAR_LEN];
char dev[CHAR_LEN];
char inode[CHAR_LEN];
char pathname[CHAR_LEN];
int main() {
printf("Hello world.n");
sprintf(filepath, "/proc/%u/maps", (unsigned)getpid());
FILE *f = fopen(filepath, "r");
printf("%-32s %-8s %-10s %-8s %-10s %sn", "address", "perms", "offset",
"dev", "inode", "pathname");
while (fgets(line, sizeof(line), f) != NULL) {
sscanf(line, "%s%s%s%s%s%s", address, perms, offset, dev, inode, pathname);
printf("%-32s %-8s %-10s %-8s %-10s %sn", address, perms, offset, dev,
inode, pathname);
}
fclose(f);
return 0;
}

我编译程序为gcc -static -O0 -g -std=gnu11 -o test_helloworld_memory_map test_helloworld_memory_map.c -lpthread.我首先运行readelf -l test_helloworld_memory_map并获得:

Elf file type is EXEC (Executable file)
Entry point 0x400890
There are 6 program headers, starting at offset 64
Program Headers:
Type           Offset             VirtAddr           PhysAddr
FileSiz            MemSiz              Flags  Align
LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
0x00000000000c9e2e 0x00000000000c9e2e  R E    200000
LOAD           0x00000000000c9eb8 0x00000000006c9eb8 0x00000000006c9eb8
0x0000000000001c98 0x0000000000003db0  RW     200000
NOTE           0x0000000000000190 0x0000000000400190 0x0000000000400190
0x0000000000000044 0x0000000000000044  R      4
TLS            0x00000000000c9eb8 0x00000000006c9eb8 0x00000000006c9eb8
0x0000000000000020 0x0000000000000050  R      8
GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000  RW     10
GNU_RELRO      0x00000000000c9eb8 0x00000000006c9eb8 0x00000000006c9eb8
0x0000000000000148 0x0000000000000148  R      1
Section to Segment mapping:
Segment Sections...
00     .note.ABI-tag .note.gnu.build-id .rela.plt .init .plt .text __libc_freeres_fn __libc_thread_freeres_fn .fini .rodata __libc_subfreeres __libc_atexit .stapsdt.base __libc_thread_subfreeres .eh_frame .gcc_except_table
01     .tdata .init_array .fini_array .jcr .data.rel.ro .got .got.plt .data .bss __libc_freeres_ptrs
02     .note.ABI-tag .note.gnu.build-id
03     .tdata .tbss
04
05     .tdata .init_array .fini_array .jcr .data.rel.ro .got

然后,我运行程序并获得:

address                          perms    offset     dev      inode      pathname
00400000-004ca000                r-xp     00000000   fd:01    12551992   /home/zeyuanhu/share/380L-Spring19/lab3/src/test_helloworld_memory_map
006c9000-006cc000                rw-p     000c9000   fd:01    12551992   /home/zeyuanhu/share/380L-Spring19/lab3/src/test_helloworld_memory_map
006cc000-006ce000                rw-p     00000000   00:00    0          /home/zeyuanhu/share/380L-Spring19/lab3/src/test_helloworld_memory_map
018ac000-018cf000                rw-p     00000000   00:00    0          [heap]
7ffc2845c000-7ffc2847d000        rw-p     00000000   00:00    0          [stack]
7ffc28561000-7ffc28563000        r--p     00000000   00:00    0          [vvar]
7ffc28563000-7ffc28565000        r-xp     00000000   00:00    0          [vdso]
ffffffffff600000-ffffffffff601000 r-xp     00000000   00:00    0          [vsyscall]

我很困惑为什么内存段的虚拟地址与"/proc/[pid]/maps"中显示的地址不同。例如,第 2 个内存段的虚拟地址0xc9eb8readelf显示,但在进程内存中,它被计算为0x6c9000。这个计算是怎么完成的?

我知道链接器指定0x400000作为第一个内存段的起始地址,进程内存显示与页面大小(4K)对齐的地址(例如,0xc9e2e对齐到0xca0000x400000)。我认为这与readelf显示的"对齐"列有关。但是,阅读 ELF 标头让我感到困惑:

p_align   This member holds the value to which the segments are
aligned in memory and in the file.  Loadable process seg‐
ments must have congruent values for p_vaddr and p_offset,
modulo the page size.  Values of zero and one mean no
alignment is required.  Otherwise, p_align should be a pos‐
itive, integral power of two, and p_vaddr should equal
p_offset, modulo p_align.

具体来说,最后一句是什么意思?

否则,p_align应该是 2 的正积分幂,p_vaddr 应等于 p_offset,模p_align。

它所说的计算公式是什么?

多谢!

CPU地址映射具有"页面"粒度,4K仍然是非常常见的页面大小。/proc/$pid/maps显示操作系统映射,但它不会显示进程在映射范围内实际关心的地址。您的进程只关心从偏移量开始的内容eb8到第一个映射页面,但 CPU(以及为您控制它的操作系统)不会费心向下映射到字节粒度,并且链接器知道这一点,因此它会使用 CPU 页面大小的块设置磁盘文件。

这意味着对于可加载段以外的段,即没有LOAD段的段,偏移量中的最后n位必须与虚拟地址中的最后n匹配;p_align字段的值是1 << n

例如,堆栈说它可以放置在任何地方,只是地址需要 16 对齐。

对于可加载的,它们至少需要页面对齐。以您示例中的第二个为例:

Offset             VirtAddr
LOAD           0x00000000000c9eb8 0x00000000006c9eb8 0x00000000006c9eb8
0x0000000000001c98 0x0000000000003db0  RW     200000

给定页面大小为 4096,偏移量的最后 12 位必须与虚拟地址的最后 12 位相同。这是因为动态链接器通常使用mmap将页面直接从文件映射到内存中,而这只能是页面粒度的。因此,实际上动态链接器确实从文件中映射了此范围的第一部分。

006c9000-006cc000                rw-p     000c9000   fd:01    12551992    
/home/zeyuanhu/share/380L-Spring19/lab3/src/test_helloworld_memory_map

进一步查看文件大小小于虚拟大小 - 其余数据将在另一个映射中映射为零:

006cc000-006ce000                rw-p     00000000   00:00    0                  
/home/zeyuanhu/share/380L-Spring19/lab3/src/test_helloworld_memory_map

如果您在0x00000000006c9000 - 0x00000000006c9eb7读取字节,您应该看到与0x00000000004c9000 - 0x00000000006c9eb7处完全相同的字节,这是因为数据段和代码段在文件中紧随其后,没有填充 - 这节省了大量磁盘空间,实际上也有助于节省 ram,因为可执行文件在块设备缓存中占用的空间更少!

最新更新