关于C#:字符串文字:它们去哪里了?

String literals: Where do they go?

我对字符串文本的分配/存储位置感兴趣。

我在这里找到了一个有趣的答案,说:

Defining a string inline actually embeds the data in the program itself and cannot be changed (some compilers allow this by a smart trick, don't bother).

但是,它与C++有关,更不用说它不麻烦了。

我很烦。= D

所以我的问题是,我的字符串在哪里,如何保存?为什么我不尝试改变它?实施是否因平台而异?有人愿意详细阐述"聪明的把戏"吗?


一种常见的技术是将字符串文本放入"只读数据"部分,该部分以只读方式映射到进程空间(这就是您不能更改它的原因)。

它确实因平台而异。例如,较简单的芯片架构可能不支持只读内存段,因此数据段是可写的。

相反,尝试找出一个使字符串文本可更改的技巧(它将高度依赖于您的平台,并可能随着时间而更改),只需使用数组:

1
char foo[] ="...";

编译器将安排从文本初始化数组,您可以修改数组。


对此没有任何答案。C和C++标准只是说字符串文本具有静态存储持续时间,任何修改它们的尝试都会给出未定义的行为,并且具有相同内容的多个字符串文字可能共享或可能不共享相同的存储。

根据您正在为其编写的系统以及它使用的可执行文件格式的功能,它们可能与程序代码一起存储在文本段中,或者它们可能具有用于初始化数据的单独段。

根据平台的不同,确定细节也会有所不同——最有可能的是包括可以告诉您将其放置在何处的工具。如果您需要的话,有些甚至可以让您控制这样的细节(例如,gnu ld允许您提供一个脚本来告诉它如何对数据、代码等进行分组)。


为什么我不尝试改变它?

因为它是未定义的行为。引用自C99 N1256草案6.7.8/32"初始化":

EXAMPLE 8: The declaration

1
char s[] ="abc", t[3] ="abc";

defines"plain" char array objects s and t whose elements are initialized with character string literals.

This declaration is identical to

1
2
char s[] = { 'a', 'b', 'c', '\0' },
t[] = { 'a', 'b', 'c' };

The contents of the arrays are modifiable. On the other hand, the declaration

1
char *p ="abc";

defines p with type"pointer to char" and initializes it to point to an object with type"array of char" with length 4 whose elements are initialized with a character string literal. If an attempt is made to use p to modify the contents of the array, the behavior is undefined.

他们去哪里?

GCC 4.8 x86-64 ELF Ubuntu 14.04:

  • char s[]:叠加
  • char *s
    • 对象文件的.rodata
    • 对象文件的.text节被转储的同一段,它具有读取和执行权限,但不具有写入权限。

程序:

1
2
3
4
5
6
7
8
#include <stdio.h>

int main() {
    char *s ="abc";
    printf("%s
"
, s);
    return 0;
}

编译和反编译:

1
2
gcc -ggdb -std=c99 -c main.c
objdump -Sr main.o

输出包含:

1
2
3
4
 char *s ="abc";
8:  48 c7 45 f8 00 00 00    movq   $0x0,-0x8(%rbp)
f:  00
        c: R_X86_64_32S .rodata

所以字符串存储在.rodata部分。

然后:

1
readelf -l a.out

包含(简化):

1
2
3
4
5
6
7
8
9
10
Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x0000000000000704 0x0000000000000704  R E    200000

 Section to Segment mapping:
  Segment Sections...
   02     .text .rodata

这意味着默认链接器脚本将.text.rodata都转储到一个可以执行但不能修改的段(Flags = R E中)。尝试修改这样的段会导致Linux中出现segfault。

如果我们对char[]也这样做:

1
 char s[] ="abc";

我们得到:

1
17:   c7 45 f0 61 62 63 00    movl   $0x636261,-0x10(%rbp)

所以它被存储在栈中(相对于%rbp),我们当然可以修改它。


仅供参考,只需备份其他答案:

标准:ISO/IEC 14882:2003规定:

2.13. String literals

  • [...]An ordinary string literal has type"array of n const char" and
    static storage duration (3.7)

  • Whether all string literals are distinct (that is, are stored in
    nonoverlapping objects) is
    implementation- defined. The effect of
    attempting to modify a string literal
    is undefined.


  • gcc生成一个.rodata段,该段在地址空间中被映射为"某处",并被标记为只读。

    VisualC++(EDCOX1,1)是为了同样的目的而制作的EDCOX1×2的部分。

    您可以查看dumpbinobjdump(在Linux上)的输出,以查看可执行文件的各个部分。

    例如。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    >dumpbin vec1.exe
    Microsoft (R) COFF/PE Dumper Version 8.00.50727.762
    Copyright (C) Microsoft Corporation.  All rights reserved.


    Dump of file vec1.exe

    File Type: EXECUTABLE IMAGE

      Summary

            4000 .data
            5000 .rdata  <-- here are strings and other read-only stuff.
           14000 .text


    它取决于可执行文件的格式。考虑到这一点的一种方法是,如果您正在进行汇编编程,那么您可以将字符串文本放入汇编程序的数据段中。您的C编译器执行类似的操作,但这完全取决于您的二进制文件是为哪个系统编译的。


    字符串文本经常分配给只读内存,使其不可变。但是,在某些编译器中,修改是可以通过"智能技巧"实现的,而智能技巧是通过"使用指向内存的字符指针"实现的。记住,有些编译器可能不允许这样做。下面是演示

    1
    2
    3
    4
    char *tabHeader ="Sound";
    *tabHeader = 'L';
    printf("%s
    "
    ,tabHeader); // Displays"Lound"


    由于这可能不同于编译器,因此最好的方法是筛选搜索字符串文字的对象转储:

    1
    objdump -s main.o | grep -B 1 str

    -s强制objdump显示所有节的全部内容时,main.o是目标文件,-B 1强制grep也在匹配前打印一行(以便您可以看到节名),str是您要搜索的字符串文字。

    在windows机器上使用gcc,在main中声明一个变量

    1
    char *c ="whatever";

    运行

    1
    objdump -s main.o | grep -B 1 whatever

    收益率

    1
    2
    Contents of section .rdata:
     0000 77686174 65766572 00000000           whatever....