字符数组/字符串如何存储在二进制文件中?

ntu*_*ntu 42 c string gcc compilation hex-editors

当我使用不同的编译器编译此代码并检查十六进制编辑器中的输出时,我希望在某处找到字符串“Nancy”。

#include <stdio.h>

int main()
{
    char temp[6] = "Nancy";
    printf("%s", temp);

    return 0;
}
Run Code Online (Sandbox Code Playgroud)
  1. 的输出文件gcc -o main main.c如下所示:

    自卫队

  2. 的输出g++ -o main main.c,我无法在任何地方找到“Nancy”。

  3. 在 Visual Studio (MSVC 1929) 中编译相同的代码,我在十六进制编辑器中看到完整的字符串:

为什么(1)中的字符串中间会出现一些随机字节?

Eri*_*hil 37

关于编译器如何在其生成的输出文件中存储数据并没有单一的规则。

\n

数据可以存储在 \xe2\x80\x9cconstant\xe2\x80\x9d 部分中。

\n

数据可以内置到指令的 \xe2\x80\x9cimmediate\xe2\x80\x9d 操作数中,其中数据被编码在编码指令的位的各个字段中。

\n

可以通过编译器生成的指令从其他数据计算数据。

\n

我怀疑您在一个地方看到 \xe2\x80\x9cNanc\xe2\x80\x9d 而在另一个地方看到 \xe2\x80\x9cy\xe2\x80\x9d 的情况是使用加载指令的编译器(可以用 \ 编写) xe2\x80\x9cmov\xe2\x80\x9d) 加载形成 \xe2\x80\x9cNanc\xe2\x80\x9d 的字节作为立即操作数,另一个加载指令加载形成 \xe2\x80\x9cy\xe2 的字节\x80\x9d 带有尾随空字符,以及其他指令,用于将加载的数据存储在堆栈上并将其地址传递给printf.

\n

您没有提供足够的信息来诊断此g++情况:您没有命名编译器或其版本号,也没有提供生成的输出的任何部分。

\n

  • 是的,这已通过 godbolt https://godbolt.org/z/Ph9cnrKEh 确认。虽然我不确定 MSVC 的做法有多健康,因为我假设“Nancy”位于某些只读常量部分中,这使得修改可能有害,但我可能会错 (8认同)
  • @Lala5th这就是`char temp[] = "Nancy"`(用只读字符串文字的副本初始化的可写本地数组)和`char *temp = "Nancy"`(设置为指向读取的指针)之间的区别-仅字符串文字,不进行复制)。`temp[0] = 'D'` 在前一种情况下是合法的,但在后一种情况下则不合法。 (4认同)
  • @Lala5th 它可能是从那里复制到本地数组中的,它不是数组本身。 (3认同)
  • @Lala5th 编译器还可以检测到“temp”从未被修改,因此可以将其视为常量。 (3认同)
  • @Eric:这个问题最好为 GCC 和 G++ 命名版本,但他们*确实*命名了编译器:它是“g++”。在 Godbolt 上,在任何优化级别编译 x86-64 时,除最旧的 (4.1) 之外的所有版本的 `g++` 都会使用立即数具体化本地非常量数组。(问题确实显示了一个完整的命令,所以我们知道优化级别是默认的“-O0”。)https://godbolt.org/z/s5TPfxn8n 显示了 C 与 C++ 模式(“-xc”与“-”) xc++`;相同的代码生成,毫不奇怪。)似乎总是双字和字存储,双字保存前 4 个字节。 (2认同)

The*_*zer 18

我在 x86-64 系统(Intel

的结果hexdump -C:

在此输入图像描述

请注意,字节序列是相同的。

所以我用gcc -S -c:

    .file   "teststr.c"
    .text
    .section    .rodata
.LC0:
    .string "%s"
    .text
    .globl  main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    endbr64
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    subq    $16, %rsp
    movq    %fs:40, %rax
    movq    %rax, -8(%rbp)
    xorl    %eax, %eax
    movl    $1668178254, -14(%rbp) # NOTE THIS PART HERE
    movw    $121, -10(%rbp)        # AND HERE
    leaq    -14(%rbp), %rax
    movq    %rax, %rsi
    leaq    .LC0(%rip), %rdi
    movl    $0, %eax
    call    printf@PLT
    movl    $0, %eax
    movq    -8(%rbp), %rdx
    xorq    %fs:40, %rdx
    je  .L3
    call    __stack_chk_fail@PLT
.L3:
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .ident  "GCC: (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0"
    .section    .note.GNU-stack,"",@progbits
    .section    .note.gnu.property,"a"
    .align 8
    .long    1f - 0f
    .long    4f - 1f
    .long    5
0:
    .string  "GNU"
1:
    .align 8
    .long    0xc0000002
    .long    3f - 2f
2:
    .long    0x3
3:
    .align 8
4:
Run Code Online (Sandbox Code Playgroud)

突出显示的值1668178254是636E614EASCII 编码中的十六进制或“cnaN”(由于 x86 是小端系统,因此字节顺序反转,变为“Nanc”),并且121是十六进制79或“y”。

因此,它使用两个移动指令,而不是从文件的字节字符串部分循环复制,因为它是一个短字符串,并且中间的“垃圾”是(我相信)以下movw指令。可能是一种优化初始化的方法,而不是通过内存逐字节循环,即使没有“正式”给编译器提供优化标志 - 就是这样,编译器可以在这方面做它想做的事情。那么,微软的编译器在编译方式上似乎更加“迂腐”,因为事实上,它显然放弃了这种优化,转而将字符串连续地放在一起。

  • _“是(我相信)下面的 movw 指令”_ — 不需要“相信”,它是明确的:`66 C7 45 F6 79 00`正是`mov word [rbp-10], 0x0079`。 (4认同)
  • 是的,这就是 objdump 这样的工具的用途。https://godbolt.org/z/nEfv98MbE 甚至有一个“二进制模式”,它可以编译+汇编并向您展示反汇编以及机器代码。(另请参阅[我对 Eric 答案的评论](https://stackoverflow.com/posts/comments/127133940),了解为什么 GCC 在启用优化的情况下执行 `mov eax, 'y'` 以避免 LCP 停顿)。我不会浪费时间查看整个二进制文件的原始十六进制转储并尝试记住数字操作码。通常,您需要知道的优化是操作码和前缀*长度*,尽管我确实记得一些常见的,例如 B8..F (4认同)
  • 通常,关闭优化后获得的优化类型更多地取决于编译器的内部结构,而不是编译器编写者是否“迂腐”。两者都是将字符放入堆栈的完全有效的方法,但我猜测也许 gcc 创建了它希望堆栈是什么样子的图片,然后使其看起来像那样,而 MSVC 创建了将字符放入堆栈的指令的图片堆栈上的数据,然后对其进行优化(如果启用) (2认同)

plu*_*ash 6

通常,编译后的程序被分成不同类型的“部分”。汇编程序文件将使用指令在它们之间进行切换。

  • 代码(“.text”)
  • 静态只读数据(“.section .rodata”)
  • 初始化的全局或静态变量(“.data”)
  • 未初始化(或零初始化)全局或静态变量(“.bss”)

C 中的字符串文字可以以两种不同的方式使用。

  • 作为指向常量数据的指针。
  • 作为数组的初始化器。

如果字符串文字用作指针,则编译器可能会将字符串数据放置在只读数据部分中。

如果使用字符串文字来初始化全局/静态数组,则编译器很可能会将数组放置在初始化数据部分中(如果数组声明为 const,则将其放置在只读数据部分中)。

但是,在您的情况下,您正在初始化的数组是一个自动局部变量。因此不能在程序启动前进行预初始化。编译器必须包含代码以在每次函数运行时对其进行初始化。

编译器可能会选择将字符串存储在只读数据位置,然后使用复制例程(内联或调用)将其复制到本地数组。它可能选择简单地生成指令来一一设置数组的元素。它可以选择生成同时设置多个数组元素的指令。

在您的示例中,MSVC 似乎已选择使用复制例程,因此该字符串按顺序出现在文件中。另一方面,gcc 选择使用 4 字节移动指令,后跟两字节移动指令,两者都以文字作为输入。所以文字被分成两部分。

PS 我注意到有些人在这个问题的其他答案上发布了 https://godbolt.org/ 链接。编译器资源管理器是一个有用的工具,但请注意,它默认隐藏汇编器输出中的节切换指令。