【问题标题】:Why are the memory addresses of string literals so different from others', on Linux?为什么在 Linux 上字符串文字的内存地址与其他的如此不同?
【发布时间】:2017-04-02 08:05:26
【问题描述】:

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

例子:

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

输出:

0x400634
0x7fffc1ef1a4c

我知道它们存储在可执行文件的.rodata 部分。操作系统之后是否有一种特殊的处理方式,所以文字最终会出现在一个特殊的内存区域(带有前导零)?该内存位置有什么优点吗?或者它有什么特别之处?

【问题讨论】:

  • 这完全取决于操作系统加载代码和分配堆栈的位置。
  • 显然是实现指定的,但 RO 数据(您的文字)通常被加载到标记为保护模式写时异常触发的单独页面中。含义:写入它会引发结构化异常。
  • 您的问题是关于 Linux、一般托管系统(带有操作系统)还是包括独立系统(通常嵌入没有操作系统)?如果仅限 Linux,则应添加 [linux] 标签。如果还有其他问题,请澄清。
  • 您的问题又回到了前面。您会发现 all 地址有“许多前导零” except 局部变量的地址,这些地址位于堆栈上,在您的情况下从地址顶部分配向下空间。
  • 要让你的字符串更像你的int i = 1,你可能想试试char h[] = "Hi"

标签: c linux memory memory-address string-literals


【解决方案1】:

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

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

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

如果您想进一步调查,那么在 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(可执行代码), .rodata 中的第一个 r--p 块,以及 .bss.data 中的以下 rw-- 块>。 (在堆和堆栈块之间是动态链接器从动态链接库中加载的块。)


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

【讨论】:

  • 谢谢,很有用!但是,如果我有多个进程,这仍然会发生。所以它不是一个接一个地排列,而是从多个进程中取出所有“初始化的全局数据”,并存储在一起?
  • @Noidea 不同的进程有不同的地址空间。一个进程中的 0xDEADBEEF(通常)与另一个进程中的 0xDEADBEEF 完全无关。上述布局在调试和块增长方面有一些明显的小优势(特别是对于堆块,虽然如果堆不能再增长,用 mmap 将堆分片也没什么大不了的)。此外,出于安全原因,实际映射的地址通常会有些随机。
  • @Noidea :不要将物理地址(对应于 RAM 中的地址)与虚拟内存地址(进程中的地址)混为一谈。 memory management unit 的工作是将虚拟地址转换为物理地址,进程使用的所有地址都通过 MMU 进行转换。每个进程都有自己的 MMU 表,由操作系统管理。
  • 几个平台的默认链接器脚本也将.rodata.text合并。
【解决方案2】:

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

【讨论】:

    【解决方案3】:

    请记住,指针 所在的位置 与指针 指向的位置 不同。更现实的(苹果对苹果)比较是

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

    【讨论】:

    • 不,h 已经是一个指向字符的指针,所以&h 没有任何用处。写h&i 是正确的,因为它们分别是引用字符串和int 的地址。
    • @underscore_d 我认为你完全误解了这个问题和我的回答。写h&i 没有“正确”或“错误”之分; OP 只是对为什么他的系统上的实际地址如此不同感到困惑。我的意思是,如果你写&h&i,或者hip,你可能会看到更多类似的地址,这个练习将(希望)帮助你理解为什么h 中的数字和&i 太不一样了。
    • @SteveSummit 指向字符串文字的指针将是另一个堆栈变量。但我想知道为什么字符串文字的地址与堆栈变量的地址如此不同。不是为什么两个堆栈变量的地址相似;)
    • @Noidea 现在你知道了,从其他答案:因为字符串文字永远不会存储在堆栈中。
    • @SteveSummit 我已经知道了,它们不在堆栈上,因为地址是如此不同。
    【解决方案4】:

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

    每个源都有一个文字池,根据链接/绑定程序的复杂程度,可以将文字池彼此相邻放置以创建一个 .rodata。

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

    考虑一下我的代码片段。我本来可以的

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

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

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

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

    【讨论】:

      【解决方案5】:
      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.
      

      【讨论】:

      • 这似乎无法回答所提出的问题。提醒一下,问题是“操作系统是否专门处理它?这种处理方式有什么优点吗?”您的回答似乎没有解决这些问题。您想编辑您的答案以更直接地解决问题吗?
      猜你喜欢
      • 1970-01-01
      • 2019-08-19
      • 2020-01-18
      • 1970-01-01
      • 1970-01-01
      • 2017-08-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多