为什么Linux系统中字符串字面值的内存地址与其他内存地址如此不同?

62

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

例如:


例如:

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

输出:

0x400634
0x7fffc1ef1a4c

我知道它们存储在可执行文件的 .rodata 部分。操作系统是否有特殊的处理方式,使得这些字面量最终位于内存的特定区域(带有前导零)?这个内存位置有什么优势,还是说有什么特别之处吗?


5
代码加载和堆栈分配完全取决于操作系统。 - Some programmer dude
8
显然是由具体实现决定,但只读数据(即您字面上的意思)通常会被加载到专门标记为受保护模式异常写触发的单独页面中。这意味着对其进行写操作将引发结构化异常。 - WhozCraig
2
你的问题是否特别涉及Linux、一般托管系统(带操作系统)或自由立式系统(通常嵌入式无操作系统)?如果只涉及Linux,请添加[linux]标记。如果是其他内容,请澄清。 - user694733
4
你的问题是前后颠倒的。你会发现所有地址都有“许多前导零”,除了本地变量的地址,它们在堆栈上,而堆栈是从地址空间顶部向下分配的。 - user207421
1
为了让你的字符串更像 int i = 1,你可以尝试使用 char h[] = "Hi" - Hagen von Eitzen
显示剩余5条评论
5个回答

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

Linux process memory layout

.rodata节是已初始化的全局数据块的受写保护的子部分。 (ELF可执行文件指定的.data节是其可写对应部分,用于将可写全局变量初始化为非零值。初始化为零的可写全局变量则放在.bss块中。这里所说的全局变量包括所有静态变量,无论其放置位置如何。)

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

如果您想进一步调查,在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*),否则行为是未定义的。


谢谢,这很有用!但如果我有多个进程,这仍然会发生。所以它不是一个接一个地布置它们,而是将多个进程的“初始化全局数据”全部取出并存储在一起? - Noidea
7
不同的进程拥有不同的地址空间。一个进程中的0xDEADBEEF通常与另一个进程中的0xDEADBEEF完全无关。上述布局与调试和块增长(特别是对于堆块而言)有一些明显的小优势,尽管如果无法再向上增长,则使用mmap来分段堆并不是很重要。此外,由于安全原因,实际映射的地址通常会随机一些。 - Petr Skocik
6
请注意不要混淆物理地址(对应于 RAM 中的地址)和虚拟内存地址(进程中的地址)。转换虚拟地址为物理地址是内存管理单元(Memory Management Unit,MMU)的工作。所有进程使用的地址都通过 MMU 进行翻译。每个进程都有自己的 MMU 表,由操作系统进行管理。 - Eric Towers
1
几个平台的默认链接器脚本也将.rodata.text合并。 - Simon Richter

15

这是因为字符串字面量具有静态存储期,也就是说它们在整个程序运行期间都会存在。这种变量可能会被存储在一个特殊的内存位置中,既不是堆栈也不是堆内存。因此地址上会有所不同。


7

记住,指针所在的位置与指针所指向的位置是不同的。更现实(可比性)的比较应该是

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指向相似的内存区域。

3
不,h已经是一个指向字符的指针,所以&h没有任何有用的作用。写h&i是正确的,因为它们都是所引用字符串和整数的地址。 - underscore_d
1
@underscore_d 我觉得你完全误解了问题和我的回答。关于写h&i没有所谓的“正确”或“错误”,OP只是对他系统上的实际地址为什么如此不同感到困惑。我的观点是,如果你写&h&i,或者hip,你可能会看到更相似的地址,这个练习将(希望)帮助你理解为什么h&i中的数字如此不同。 - Steve Summit
3
@SteveSummit 指向字符串文字的指针将是另一个栈变量。但我想知道为什么字符串字面值的地址与栈变量的地址非常不同。不是为什么两个栈变量的地址相似 ;) - Noidea
@Noidea 现在你知道了,从其他答案中可以看出:因为字符串文字永远不会存储在堆栈中。 - Steve Summit
@SteveSummit 嗯,我已经知道它们不在堆栈上了,因为地址非常不同。 - Noidea

1
考虑到文字量是只读变量,还有一个字面池的概念。字面池是程序独特文字量的集合,其中重复的常量被丢弃,因为引用会合并成一个。
每个源都有一个字面池,根据链接/绑定程序的复杂程度,字面池可以相邻放置以创建一个.rodata。
字面池也没有保证是只读保护的。语言编译器设计将其视为如此。
考虑我的代码片段。我可以
const char *cp="hello world"; const char *cp1="hello world";
好的编译器会认识到在该源代码中,只读的文字量cp、cp1指向相同的字符串,并使cp1指向cp的文字量,丢弃第二个。
还有一点。字面池可能是256字节或不同值的倍数。如果池数据少于256字节,则会用十六进制零填充松弛部分。
不同的编译器遵循通用的开发标准,允许使用C编译的模块与使用汇编语言或其他语言编译的模块进行链接。这两个文本池在.rodata中依次放置。

0
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.

1
这似乎没有回答所问的问题。作为提醒,问题是“操作系统是否会特别处理它?这种处理方式有什么优势?”您的回答似乎没有回答这些问题。您是否想编辑您的回答以更直接地回答问题? - D.W.

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