从C转换为汇编时,以null结尾的字符串在哪里?

时间:2016-08-09 11:17:40

标签: c linux gcc x86

我制作了两个程序来输出两个字符串,一个在汇编中,另一个在C中。 这是汇编程序:

.section .data
string1:
.ascii "Hola\0"
string2:
.ascii "Adios\0"

.section .text
.globl _start
_start:

pushl $string1
call puts
addl $4, %esp

pushl $string2
call puts
addl $4, %esp

movl $1, %eax
movl $0, %ebx
int $0x80

我用

构建程序
as test.s -o test.o
ld -dynamic-linker /lib/ld-linux.so.2 -o test test.o -lc

输出符合预期

Hola
Adios

这是C程序:

#include <stdio.h>
int main(void)
{
    puts("Hola");
    puts("Adios");
    return 0;
}

我得到了预期的输出,但是当使用gcc -S(OS是Debian 32位)将此C程序转换为程序集时,输出程序集源代码在两个字符串中都不包含空字符,如下所示:

    .file   "testc.c"
    .section    .rodata
.LC0:
    .string "Hola"
.LC1:
    .string "Adios"
    .text
    .globl  main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    leal    4(%esp), %ecx
    .cfi_def_cfa 1, 0
    andl    $-16, %esp
    pushl   -4(%ecx)
    pushl   %ebp
    .cfi_escape 0x10,0x5,0x2,0x75,0
    movl    %esp, %ebp
    pushl   %ecx
    .cfi_escape 0xf,0x3,0x75,0x7c,0x6
    subl    $4, %esp
    subl    $12, %esp
    pushl   $.LC0
    call    puts
    addl    $16, %esp
    subl    $12, %esp
    pushl   $.LC1
    call    puts
    addl    $16, %esp
    movl    $0, %eax
    movl    -4(%ebp), %ecx
    .cfi_def_cfa 1, 0
    leave
    .cfi_restore 5
    leal    -4(%ecx), %esp
    .cfi_def_cfa 4, 4
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .ident  "GCC: (Debian 4.9.2-10) 4.9.2"
    .section    .note.GNU-stack,"",@progbits

我的两个问题是:

1)为什么gcc生成的汇编代码不会在两个字符串的末尾附加空字符?我以为C是自动完成的。

2)如果我跳过手工制作的汇编代码中的空字符,我会得到这个输出:

HolaAdios
Adios

我明白为什么我在第一行得到“HolaAdios”部分,但为什么程序在“Adios”部分之后成功结束,如果它不是以空终止的?

2 个答案:

答案 0 :(得分:5)

  1. .string始终附加一个空终结符,如here所示。
  2. 嗯,你可以自己检查一下。 puts只是继续,直到它看到一个空字节。 \x00是非常常见的,附近必须有一个,所以它可以工作(可能是由于.rodata的部分对齐)。

答案 1 :(得分:0)

只是添加更多细节:

你的第二个字符串是偶然的,因为在.data部分后面没有任何内容。你动态链接glibc,它也有一个.data部分,它被映射到你的进程的地址空间。这是一个私有映射,但我认为映射,而不是复制,所以它是页面对齐的。保存可执行文件数据段的页面的其余部分用零填充。 (ABI可能无法保证这一点,但Linux必须采取措施避免泄漏内核数据)。

当您的可执行文件加载到内存中时,数据将与文本分开加载。 See this answer关于部分(链接器关心的)和可执行段(程序加载器关心的)之间的区别。

请注意,gcc将字符串常量放在.rodata部分中,链接器将其放置在可执行文件的文本段中,以及.text部分:只读,以便它可以在多个部分之间共享进程运行相同的可执行文件截面默认情况下使用填充对齐,因此即使您将字符串放在.rodata而没有零终结符,也会在第二个之后填充零。

如果碰巧在右对齐边界处结束(例如,长度是16的倍数,或其他东西),则不会发生这种情况。

顺便说一句,您可以使用strace ./string-test确认字符串后面没有任何非打印垃圾字符。您可以看到:write(1, "Adios\n", 6) = 6

.string.asciz的同义词。本手册使用不同的语言来描述它们处理反斜杠转义序列的事实,并附加一个零字节,但它们做同样的事情。 GNU汇编程序有很多同义词可以与许多不同的Unix供应商提供的汇编程序兼容,所以当gcc使用.zero但clang使用.skip之类的东西时,实际上没有区别,这可能会令人困惑。

  

我用......构建程序

您使用的命令仅适用于32位系统。在64位主机上,您将构建一个仍使用32位系统调用ABI的64位二进制文​​件。 (以及32位动态链接器路径,因此它甚至不会偶然工作,即使静态数据地址是低32位,因此可以传递给sys_write的32位包装器。)

另外,我建议您调用源文件test.S。 capital-S通常用于手写asm源。您可以与gcc -m32 -nostartfiles test.S -o test进行汇编和链接,以便以与手动相同的方式进行汇编和链接。

有关在Linux上构建asm的完整详细信息,请参阅此问答: Assembling 32-bit binaries on a 64-bit system (GNU toolchain)

另请参阅标记wiki以获取许多有趣的链接。