为什么 Linux 上字符串文字的内存地址与其他字符串文字的内存地址如此不同?

Why are the memory addresses of string literals so different from others', on Linux?

我注意到字符串文字在内存中的地址与其他常量和变量 (Linux OS) 非常不同:它们有许多前导零(未打印)。

示例:

const char *h = "Hi";
int i = 1;
printf ("%p\n", (void *) h);
printf ("%p\n", (void *) &i);

输出:

0x400634
0x7fffc1ef1a4c

我知道它们存储在可执行文件的 .rodata 部分。 OS 之后是否有特殊的处理方式,所以文字最终会出现在特殊的内存区域(前导零)?该内存位置是否有任何优势或有什么特别之处?

printf ("%p\n", h); // h is the address of "Hi", which is in the rodata or other segments of the application.
printf ("%p\n", &i); // I think "i" is not a global variable, so &i is in the stack of main. The stack address is by convention in the top area of the memory space of the process.

那是因为字符串文字具有静态存储持续时间。也就是说,他们将在整个程序中存活。这些变量可能存储在一个特殊的内存位置,该位置既不在所谓的堆上,也不在堆栈上。因此地址不同。

进程内存在 Linux 上的布局方式(来自 http://www.thegeekstuff.com/2012/03/linux-processes-memory-layout/):

.rodata 部分是 Initialized Global Data 块的写保护子部分。 (ELF 可执行文件指定 .data 的部分是初始化为非零值的可写全局变量的可写对应部分。初始化为零的可写全局变量转到 .bss 块。这里的全局变量是指全局变量和所有 static 变量,无论放置如何。)

图片应该解释你的地址的数值。

如果您想进一步调查,那么在 Linux 您可以检查 /proc/$pid/maps 描述 运行 进程内存布局的虚拟文件。您不会获得保留的(以点开头的)ELF 部分名称,但您可以通过查看其内存保护标志来猜测内存块来自哪个 ELF 部分。例如,运行

$ cat /proc/self/maps #cat's memory map

给我

00400000-0040b000 r-xp 00000000 fc:00 395465                             /bin/cat
0060a000-0060b000 r--p 0000a000 fc:00 395465                             /bin/cat
0060b000-0060d000 rw-p 0000b000 fc:00 395465                             /bin/cat
006e3000-00704000 rw-p 00000000 00:00 0                                  [heap]
3000000000-3000023000 r-xp 00000000 fc:00 3026487                        /lib/x86_64-linux-gnu/ld-2.19.so
3000222000-3000223000 r--p 00022000 fc:00 3026487                        /lib/x86_64-linux-gnu/ld-2.19.so
3000223000-3000224000 rw-p 00023000 fc:00 3026487                        /lib/x86_64-linux-gnu/ld-2.19.so
3000224000-3000225000 rw-p 00000000 00:00 0
3000400000-30005ba000 r-xp 00000000 fc:00 3026488                        /lib/x86_64-linux-gnu/libc-2.19.so
30005ba000-30007ba000 ---p 001ba000 fc:00 3026488                        /lib/x86_64-linux-gnu/libc-2.19.so
30007ba000-30007be000 r--p 001ba000 fc:00 3026488                        /lib/x86_64-linux-gnu/libc-2.19.so
30007be000-30007c0000 rw-p 001be000 fc:00 3026488                        /lib/x86_64-linux-gnu/libc-2.19.so
30007c0000-30007c5000 rw-p 00000000 00:00 0
7f49eda93000-7f49edd79000 r--p 00000000 fc:00 2104890                    /usr/lib/locale/locale-archive
7f49edd79000-7f49edd7c000 rw-p 00000000 00:00 0
7f49edda7000-7f49edda9000 rw-p 00000000 00:00 0
7ffdae393000-7ffdae3b5000 rw-p 00000000 00:00 0                          [stack]
7ffdae3e6000-7ffdae3e8000 r--p 00000000 00:00 0                          [vvar]
7ffdae3e8000-7ffdae3ea000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

第一个r-xp块肯定来自.text(可执行代码), 第一个 r--p 块来自 .rodata,接下来的 rw-- 块来自 .bss.data。 (在堆和堆栈块之间是动态链接器从动态链接库加载的块。)


注意: 为了符合标准,您应该将 "%p"int* 转换为 (void*) 否则行为未定义.

请记住,指针 指向 的位置不同于指针 指向 的位置。更现实的(同类)比较是

printf ("%p\n", (void *) &h);
printf ("%p\n", (void *) &i);

我想您会发现 hp 有相似的地址。或者,另一个更现实的比较是

static int si = 123;
int *ip = &si;
printf ("%p\n", (void *) h);
printf ("%p\n", (void *) ip);

我怀疑您会发现 hip 指向相似的内存区域。

考虑到文字是只读变量,还有文字池的概念。文字池是程序唯一文字的集合,其中重复的常量被丢弃,因为引用被合并为一个。

每个源都有一个文字池,根据 link/bind 程序的复杂程度,文字池可以并排放置以创建一个 .rodata。

也不能保证文字池是只读保护的。语言虽然编译器设计如此对待它。

考虑我的代码片段。我可以

const char *cp="hello world";
const char *cp1="hello world";

优秀的编译器会认识到在该源代码中,只读文字 cp、cp1 指向相同的字符串,并且会使 cp1 指向 cp 的文字,丢弃第二个。

还有一点。文字池可能是 256 字节的倍数或不同的值。如果池数据小于 256 字节,松弛部分将用十六进制零填充。

不同的编译器,遵循共同的开发标准,允许用C编译的模块,link用汇编语言编译的模块编译 或其他语言。两个文字池连续放在.rodata.