【问题标题】:Extra bytes at the end of a DOS .COM file, compiled with GCC使用 GCC 编译的 DOS .COM 文件末尾的额外字节
【发布时间】:2019-10-15 12:24:39
【问题描述】:

我有以下 C 源文件,其中包含一些 asm 块,它们通过调用 DOS 系统调用来实现打印和退出例程。

__asm__(
    ".code16gcc;"
    "call dosmain;"
    "mov $0x4C, %AH;"
    "int $0x21;"
);

void print(char *str)
{
    __asm__(
        "mov $0x09, %%ah;"
        "int $0x21;"
        : // no output
        : "d"(str)
        : "ah"
    );
}

void dosmain()
{
    // DOS system call expects strings to be terminated by $.
    print("Hello world$");
}

链接脚本文件和构建脚本文件是这样的,

OUTPUT_FORMAT(binary)
SECTIONS
{
    . = 0x0100;
    .text :
    {
        *(.text);
    }
    .data :
    {
        *(.data);
        *(.bss);
        *(.rodata);
    }
    _heap = ALIGN(4);
}
gcc -fno-pie -Os -nostdlib -ffreestanding -m16 -march=i386 \
-Wl,--nmagic,--script=simple_dos.ld simple_dos.c -o simple_dos.com

我习惯于在汇编中构建 .COM 文件,并且我知道 dos 文件的结构。但是,对于使用 GCC 生成的 .COM 文件,我最后会得到一些额外的字节,我不知道为什么。 (阴影区域和下面方框内的字节是预期的,其他所有内容都下落不明)。

[]

我的直觉是这些是 GCC 使用的一些静态存储。我认为这可能是由于程序中的字符串。因此,我评论了print("Hello world$"); 行,但额外的字节仍然存在。如果有人知道发生了什么并告诉如何防止 GCC 在输出中插入这些字节,这将非常有帮助。

源代码在这里:Github

PS:目标文件也包含这些额外的字节。

【问题讨论】:

  • 旁注:您的打印功能不安全。 int $0x21 不是在 AL 中返回吗?但是你没有告诉编译器,只有 AH。最好告诉它整个 EAX 已被破坏。
  • @PeterCordes 没错。谢谢。你知道为什么会有额外的字节吗?
  • 不知道,我对链接器脚本不太熟悉。您是否检查了.o 以查看这些字节是否存在于目标文件中?不过,我注意到您没有指定 --oformat 选项,因此您没有告诉 LD 制作没有元数据的平面二进制文件。
  • @PeterCordes 是的,它们也在目标文件中。
  • 我不会使用gcc 链接可执行文件,而是直接调用ld。这样您就不必担心gcc 添加任何额外的目标文件或库。

标签: gcc x86 dos inline-assembly linker-scripts


【解决方案1】:

由于您使用的是本机编译器而不是 i686(或 i386)交叉编译器,因此您可以获得大量额外信息。它相当依赖于编译器配置。我建议执行以下操作以删除不需要的代码生成和部分:

  • 使用 GCC 选项 -fno-asynchronous-unwind-tables 消除任何 .eh_frame 部分。在这种情况下,这是在您的 DOS COM 程序末尾附加不需要的数据的原因
  • 使用 GCC 选项 -static 在不重定位的情况下构建以避免任何形式的动态链接。
  • 让 GCC 使用 -Wl--build-id=none 选项传递给链接器,以避免不必要地生成任何 .note.gnu.build-id 部分。
  • 修改链接描述文件以丢弃任何.comment 部分。

您的构建命令可能如下所示:

gcc -fno-pie -static -Os -nostdlib -fno-asynchronous-unwind-tables -ffreestanding \
-m16 -march=i386 -Wl,--build-id=none,--nmagic,--script=simple_dos.ld simple_dos.c \
-o simple_dos.com

我会将您的链接器脚本修改为如下所示:

OUTPUT_FORMAT(binary)
SECTIONS
{
    . = 0x0100;
    .text :
    {
        *(.text*);
    }
    .data :
    {
        *(.data);
        *(.rodata*);
        *(.bss);
        *(COMMON)
    }
    _heap = ALIGN(4);

    /DISCARD/ : { *(.comment); }
}

除了添加 /DISCARD/ 指令以消除任何 .comment 部分之外,我还在 .bss 旁边添加了 *(COMMON)。两者都是 BSS 部分。我还将它们移到数据部分之后,因为如果它们出现在其他部分之后,它们不会占用 .COM 文件中的空间。我还将*(.rodata); 更改为*(.rodata*); 并将*(.text); 更改为*(.text*);,因为GCC 可以生成以.rodata.text 开头但具有不同后缀的部分名称。


内联汇编

与您询问的问题无关,但很重要。在这个内联汇编中:

__asm__(
    "mov $0x09, %%ah;"
    "int $0x21;"
    : // no output
    : "d"(str)
    : "ah"
);

Int 21h/AH=9h 也破坏了 AL。您应该使用ax 作为破坏者。

由于您通过寄存器传递数组的地址,因此您还需要添加一个memory clobber,以便编译器在发出内联汇编之前将整个数组实现到内存中。约束"d"(str) 仅告诉编译器您将使用指针作为输入,而不是指针指向的位置。

如果您在 -O3 进行优化编译,您可能会发现以下版本的程序甚至没有您的字符串 "Hello world$",因为这个错误:

__asm__(
        ".code16gcc;"
        "call dosmain;"
        "mov $0x4C, %AH;"
        "int $0x21;"
);

void print(char *str)
{
        __asm__(
                "mov $0x09, %%ah;"
                "int $0x21;"
                : // no output
                : "d"(str)
                : "ax");
}

void dosmain()
{
        char hello[] = "Hello world$";
        print(hello);
}

dosmain 的生成代码在堆栈上为字符串分配空间,但在打印字符串之前从未将字符串放入堆栈:

00000100 <print-0xc>:
 100:   66 e8 12 00 00 00       calll  118 <dosmain>
 106:   b4 4c                   mov    $0x4c,%ah
 108:   cd 21                   int    $0x21
 10a:   66 90                   xchg   %eax,%eax

0000010c <print>:
 10c:   67 66 8b 54 24 04       mov    0x4(%esp),%edx
 112:   b4 09                   mov    $0x9,%ah
 114:   cd 21                   int    $0x21
 116:   66 c3                   retl

00000118 <dosmain>:
 118:   66 83 ec 10             sub    $0x10,%esp
 11c:   67 66 8d 54 24 03       lea    0x3(%esp),%edx
 122:   b4 09                   mov    $0x9,%ah
 124:   cd 21                   int    $0x21
 126:   66 83 c4 10             add    $0x10,%esp
 12a:   66 c3                   retl

如果您将内联程序集更改为包含这样的 "memory" clobber:

void print(char *str)
{
        __asm__(
                "mov $0x09, %%ah;"
                "int $0x21;"
                : // no output
                : "d"(str)
                : "ax", "memory");
}

生成的代码可能看起来类似

00000100 <print-0xc>:
 100:   66 e8 12 00 00 00       calll  118 <dosmain>
 106:   b4 4c                   mov    $0x4c,%ah
 108:   cd 21                   int    $0x21
 10a:   66 90                   xchg   %eax,%eax

0000010c <print>:
 10c:   67 66 8b 54 24 04       mov    0x4(%esp),%edx
 112:   b4 09                   mov    $0x9,%ah
 114:   cd 21                   int    $0x21
 116:   66 c3                   retl

00000118 <dosmain>:
 118:   66 57                   push   %edi
 11a:   66 56                   push   %esi
 11c:   66 83 ec 10             sub    $0x10,%esp
 120:   67 66 8d 7c 24 03       lea    0x3(%esp),%edi
 126:   66 be 48 01 00 00       mov    $0x148,%esi
 12c:   66 b9 0d 00 00 00       mov    $0xd,%ecx
 132:   f3 a4                   rep movsb %ds:(%si),%es:(%di)
 134:   67 66 8d 54 24 03       lea    0x3(%esp),%edx
 13a:   b4 09                   mov    $0x9,%ah
 13c:   cd 21                   int    $0x21
 13e:   66 83 c4 10             add    $0x10,%esp
 142:   66 5e                   pop    %esi
 144:   66 5f                   pop    %edi
 146:   66 c3                   retl

Disassembly of section .rodata.str1.1:

00000148 <_heap-0x10>:
 148:   48                      dec    %ax
 149:   65 6c                   gs insb (%dx),%es:(%di)
 14b:   6c                      insb   (%dx),%es:(%di)
 14c:   6f                      outsw  %ds:(%si),(%dx)
 14d:   20 77 6f                and    %dh,0x6f(%bx)
 150:   72 6c                   jb     1be <_heap+0x66>
 152:   64 24 00                fs and $0x0,%al

可以使用变量通过a 约束传递子函数 9 并使用 + 将其标记为输入/输出的内联程序集的替代版本(因为 AX 的返回值被破坏)可以完成这样:

void print(char *str)
{
    unsigned short int write_fun = (0x09<<8) | 0x00;
    __asm__ __volatile__ (
        "int $0x21;"
        : "+a"(write_fun)
        : "d"(str)
        : "memory"
    );
}

建议:不要将 GCC 用于 16 位代码生成。内联汇编是difficult to get right,您可能会在低级例程中使用相当多的它。您可以查看Smaller CBruce's C compilerOpenwatcom C 作为替代方案。都可以生成DOS COM程序。

【讨论】:

  • .comment 部分没有设置 ALLOC 标志,因此不会在可执行文件中获得输出。
  • @RossRidge :该建议实际上更多地与不要将相应的 ELF 文件(如果他们选择生成它)与可能使他们混淆或导致他们相信它们成为二进制输出文件也是如此。
【解决方案2】:

额外的数据很可能是 DWARF 展开信息。您可以使用-fno-asynchronous-unwind-tables 选项阻止 GCC 生成它。

您还可以通过将以下内容添加到链接描述文件的 SECTIONS 指令中,让 GNU 链接器丢弃展开信息:

/DISCARD/ : 
{
     *(.eh_frame)
}

还请注意,生成的 COM 文件将比您预期的大一个字节,因为字符串末尾的空字节。

【讨论】:

    猜你喜欢
    • 2015-06-06
    • 1970-01-01
    • 2019-06-16
    • 2013-03-20
    • 1970-01-01
    • 2021-08-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多