为什么 gcc 为 memcpy 复制 rodata 字符串?如何避免?
Why gcc duplicates rodata string for memcpy? How to avoid it?
出于某种原因,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
符号"pattern"为原数组
符号 "str1" 重复
符号 "tmpbuf" 是目标缓冲区,我想将 "pattern".
复制到其中
查看生成的程序集,我发现 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] [=14=]0"
.section .rodata.str1.1,"aMS",%progbits,1
.LC0: /*duplicate string*/
.ascii "[SOME TEST PATTERN TO CALCULATE SUM FROM] [=14=]0"
.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 的 arm-none-eabi-gcc 版本(最新版本 developer.arm.com)。
我也尝试过各种优化。仅当使用 -O0 时才不会出现重复。对于其他人来说是这样。
在更大的应用程序中,出现了这个问题,事实证明 memcpy 复制了重复的字符串而不是原始字符串 - 这是通过用二进制替换原始字符串来确定的。我需要 memcpy 才能使用原始字符串。
您观察到的行为是标准明确规定的。在
static const char pattern[] = "[SOME TEST PATTERN TO CALCULATE SUM FROM] ";
你有一个变量 pattern 的声明和一个字符串文字形式的初始化器。标准的Paragraph 6.4.5/6规定
In translation phase 7, a byte or code of value zero is appended to
each multibyte character sequence that results from a string literal
or literals. The multibyte character sequence is then used to
initialize an array of static storage duration and length just
sufficient to contain the sequence.
(强调。)结果数组具有静态存储持续时间意味着,至少在原则上,必须在程序中为其预留内存。这就是您以 str1.1
的形式看到的内容。但是您还使用该字符串来初始化数组,以便该数组获得相同的字符序列,并且它还占用二进制文件中的内存,因为它也具有静态存储持续时间,因为它是在文件范围内声明的结果。
原则上,GCC 应该能够优化掉多余的数组。特别是,选项 -fmerge-constants
应该执行此操作,但这包含在除 -O0
之外的所有优化级别中,因此您没有看到这样的合并是令人惊讶的,但合并可能会执行在 link 时间,因此您看到的是在 linking.
之前查看目标文件的无意义的产物
您还应该能够通过将 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', ']', ' ', '[=12=]' };
这让你得到 pattern
作为数组,而不是指针,并且没有单独的数组用于字符串文字。它很难看而且更难维护,但是从字符串文字形式转换成那种形式并不难——我花了大约 30 秒才完成。但是,如果您这样做,请不要忘记添加一个明确的字符串终止符,如上所示。
出于某种原因,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
符号"pattern"为原数组 符号 "str1" 重复 符号 "tmpbuf" 是目标缓冲区,我想将 "pattern".
复制到其中查看生成的程序集,我发现 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] [=14=]0"
.section .rodata.str1.1,"aMS",%progbits,1
.LC0: /*duplicate string*/
.ascii "[SOME TEST PATTERN TO CALCULATE SUM FROM] [=14=]0"
.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 的 arm-none-eabi-gcc 版本(最新版本 developer.arm.com)。
我也尝试过各种优化。仅当使用 -O0 时才不会出现重复。对于其他人来说是这样。
在更大的应用程序中,出现了这个问题,事实证明 memcpy 复制了重复的字符串而不是原始字符串 - 这是通过用二进制替换原始字符串来确定的。我需要 memcpy 才能使用原始字符串。
您观察到的行为是标准明确规定的。在
static const char pattern[] = "[SOME TEST PATTERN TO CALCULATE SUM FROM] ";
你有一个变量 pattern 的声明和一个字符串文字形式的初始化器。标准的Paragraph 6.4.5/6规定
In translation phase 7, a byte or code of value zero is appended to each multibyte character sequence that results from a string literal or literals. The multibyte character sequence is then used to initialize an array of static storage duration and length just sufficient to contain the sequence.
(强调。)结果数组具有静态存储持续时间意味着,至少在原则上,必须在程序中为其预留内存。这就是您以 str1.1
的形式看到的内容。但是您还使用该字符串来初始化数组,以便该数组获得相同的字符序列,并且它还占用二进制文件中的内存,因为它也具有静态存储持续时间,因为它是在文件范围内声明的结果。
原则上,GCC 应该能够优化掉多余的数组。特别是,选项 -fmerge-constants
应该执行此操作,但这包含在除 -O0
之外的所有优化级别中,因此您没有看到这样的合并是令人惊讶的,但合并可能会执行在 link 时间,因此您看到的是在 linking.
您还应该能够通过将 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', ']', ' ', '[=12=]' };
这让你得到 pattern
作为数组,而不是指针,并且没有单独的数组用于字符串文字。它很难看而且更难维护,但是从字符串文字形式转换成那种形式并不难——我花了大约 30 秒才完成。但是,如果您这样做,请不要忘记添加一个明确的字符串终止符,如上所示。