c语言 - 为什么 gcc 复制 rodata 字符串用于内存?如何避免?



出于某种原因,GCC 将 const char 字符串的内容复制到单独的 rodata 区域中,我不明白。 我编译提供的代码:

static const char pattern[] = "[SOME TEST PATTERN TO CALCULATE SUM FROM] ";
static char tmpbuf[sizeof(pattern) + 1];
uint16_t sum(char *buf, int size)
{
uint16_t ret = 0;
for(int i = 0; i < size; ++i)
ret += buf[i];
return ret;
}
void getPattern(char **retbuf)
{
memcpy(tmpbuf, pattern, sizeof(tmpbuf) -1);
*retbuf = tmpbuf;
}
int main(int argc, char *argv[])
{
getPattern(&argv[0]);
return sum((char *)pattern, sizeof(pattern) - 2) > 0;
}
void _exit(int status)
{
while(1)
{
asm("nop");
}
}

使用ARM GCC 编译器,使用命令:

arm-none-eabi-gcc -Os dbstr.c -o dbstr -Wl,-Map,"dbstr.map" -fdata-sections

在生成的二进制文件中,即使它被剥离,我也发现字符串:

"[SOME TEST PATTERN TO CALCULATE SUM FROM] "

重复。

查看符号地图,我发现:

.rodata.pattern
0x000087d8       0x2b ... ccumYoyx.o
.rodata.str1.1
0x00008803       0x2b ... ccumYoyx.o
and
.bss.tmpbuf    0x00018ca0       0x2c ... ccumYoyx.o

符号"模式"是原始数组 符号"str1"重复 符号"tmpbuf"是目标缓冲区,我想将"模式"复制到其中。

查看生成的程序集,我发现memcpy使用编译器创建的副本:

getPattern:
@ Function supports interworking.
@ args = 0, pretend = 0, frame = 0
@ frame_needed = 0, uses_anonymous_args = 0
->  ldr r3, .L6
push    {r4, lr}
mov r2, #43
mov r4, r0
ldr r1, .L6+4
mov r0, r3
bl  memcpy
...
.L6:
.word   .LANCHOR0
->  .word   .LC0
...
pattern:
.ascii  "[SOME TEST PATTERN TO CALCULATE SUM FROM] 00"
.section    .rodata.str1.1,"aMS",%progbits,1
.LC0: /*duplicate string*/
.ascii  "[SOME TEST PATTERN TO CALCULATE SUM FROM] 00"
.ident  "GCC: (GNU Tools for Arm Embedded Processors 8-2018-q4-major) 8.2.1 20181213 (release) [gcc-8-branch revision 267074]"

我检查了它是否发生在从 6-2017-q1-update 到 8-2018-q4-major(最新版本在 developer.arm.com 上可用)的 arm-none-eabi-gcc 版本中。

我还尝试使用各种优化。重复不仅在使用 -O0 时发生。对于其他人来说,它确实如此。

在更大的应用程序中,发生了这个问题,事实证明memcpy复制了重复的字符串而不是原始字符串 - 它是通过在二进制中替换原始字符串来确定的。我需要memcpy来使用原始字符串。

您观察到的行为由标准明确指定。 在

static const char pattern[] = "[SOME TEST PATTERN TO CALCULATE SUM FROM] ";

,您有一个变量模式的声明和一个字符串文本形式的初始值设定项。 标准第6.4.5/6段规定

在转换阶段 7 中,值为零的字节或代码附加到 由字符串文本生成的每个多字节字符序列 或文字。然后使用多字节字符序列 初始化静态存储持续时间和长度的数组足以包含序列。

(强调后加。生成的数组具有静态存储持续时间意味着,至少在原则上,必须在程序中为其保留内存。 这就是您以str1.1的形式看到的. 但是,您也使用该字符串来初始化数组,以便该数组获得相同的字符序列,并且还会占用二进制文件中的内存,因为它由于在文件范围内声明而具有静态存储持续时间。

原则上,GCC 应该能够优化多余的数组。 特别是,选项-fmerge-constants应该这样做,但这包含在除-O0以外的所有优化级别中,因此您没有看到这种合并是令人惊讶的,但合并可能会在链接时执行,因此您看到的是链接前查看目标文件的无意义工件。

您还应该能够通过将pattern声明为指针而不是数组来避免复制:

static const char * const pattern = "[SOME TEST PATTERN TO CALCULATE SUM FROM] ";

请注意,尽管结果可以以与数组版本相同的许多方式使用,但它在语义上并不相同。 如果将sizeof*&_Alignof运算符应用于pattern,则会看到差异。


更新:

另一个更丑陋的解决方法是完全避免字符串文字,如下所示:

static const char pattern[] = {
'[', 'S', 'O', 'M', 'E', ' ', 'T', 'E', 'S', 'T', ' ', 'P', 'A', 'T',
'T', 'E', 'R', 'N', ' ', 'T', 'O', ' ', 'C', 'A', 'L', 'C', 'U', 'L',
'A', 'T', 'E', ' ', 'S', 'U', 'M', ' ', 'F', 'R', 'O', 'M', ']', ' ', '' };

这样一来,pattern就是数组,而不是指针,也没有字符串文字的单独数组。 它很丑陋,更难维护,但从字符串文字形式转换为字符串文字形式并不难——我花了大约 30 秒来做到这一点。 但是,如果您这样做,请不要忘记添加显式字符串终止符,如上所述。

最新更新