【问题标题】:Why gcc duplicates rodata string for memcpy? How to avoid it?为什么 gcc 为 memcpy 复制 rodata 字符串?如何避免?
【发布时间】:2019-05-06 11:08:28
【问题描述】:

出于某种原因,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”是目标缓冲区,我想将“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] \000"
    .section    .rodata.str1.1,"aMS",%progbits,1
.LC0: /*duplicate string*/
    .ascii  "[SOME TEST PATTERN TO CALCULATE SUM FROM] \000"
    .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 才能使用原始字符串。

【问题讨论】:

  • 您可以尝试将pattern 变量也声明为volatile,以防止对其进行任何优化。拥有constvolatile 是完全合法的,因此声明将是:static const volatile char pattern[] = "[SOME TEST PATTERN TO CALCULATE SUM FROM] ";
  • 您可以尝试优化尺寸 (-Os)。如果可行,那么可能有一个特定的优化选项可以控制这种行为(GCC 优化是高度可调的)。
  • 并且要清楚,这里的问题是二进制文件比它需要的大,是吗?如果它产生不正确的行为,那么我们希望看到实际展示这种不当行为的代码,以便诊断该问题。
  • @JohnBollinger 是对的,为了给出正确的答案,我们需要实际问题和演示它的代码。尽管如此,当const 变量不像编译器可能认为的const 时,我的评论仍然适用。例如,当使用能够擦除其部分内存并用其他数据替换它的 uC 时,可能会发生这种情况。编译器不会知道这种行为,因为它不是标准 C。这可以通过使用 volatile 关键字来解决。
  • 我忽略了这一点,@andreee。我还忽略了 OP 正在查看目标文件,而不是链接的可执行文件,这也可能会有所不同。

标签: c string gcc duplicates memcpy


【解决方案1】:

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

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

你有一个变量模式的声明和一个字符串文字形式的初始化器。标准的Paragraph 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*&amp;_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', ']', ' ', '\0' };

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

【讨论】:

  • 这解释了发生了什么,但额外的数组在链接时没有优化。显式添加 -fmerge-constants 没有任何效果。我与为 x86 编译的这段代码有相同的结果 - 也许它应该被报告为 GCC 中的错误?
  • 实际上,将两个数组声明为非静态并不会改变任何东西 - 二进制中仍然存在两次。从模式中删除 const sepcifier 只会导致字符串在二进制中出现一次。由于添加 volatile 会改变这种行为,它看起来更像是某种优化问题,而不是初始化。
  • static 关键字与它无关。所有文件范围声明都有静态存储持续时间。结合这样的声明,static 关键字指定内部链接。 (不同的规则适用于块范围变量。)
  • 我无法解释为什么当您使用应该具有该效果的优化标志时 GCC 不合并相同的字符串常量(当然,假设您确实正在查看由 GCC 创建的二进制文件在编译时和链接时都受这些标志的影响)。但是,如果 GCC 不这样做,那么原因并不重要,尽管您可以考虑提交 bug 票。我添加了第二个解决方法,尽管“解决方法”是一种描述编写直接描述您真正想要的代码的奇怪方式。
  • 好的,第二个解决方法有效。按照这个答案,每当我用它初始化静态存储持续时间字符数组时,都应该出现字符串文字的第二个副本。但是当我从上面的代码中删除 getPattern 函数时,声明保持不变,但这个字符串只有一次出现在二进制文件中......我没有看到第二个字符串的任何用法,因为模式符号已经在同一部分( rodata) 作为编译器创建的辅助字符串,并且已经初始化。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-07-15
  • 1970-01-01
  • 1970-01-01
  • 2016-09-15
  • 2013-03-21
  • 1970-01-01
相关资源
最近更新 更多