DOS .COM 文件末尾的额外字节,使用 GCC 编译

Extra bytes at the end of a DOS .COM file, compiled with GCC

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

__asm__(
    ".code16gcc;"
    "call dosmain;"
    "mov [=10=]x4C, %AH;"
    "int [=10=]x21;"
);

void print(char *str)
{
    __asm__(
        "mov [=10=]x09, %%ah;"
        "int [=10=]x21;"
        : // 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: 目标文件也包含这些额外的字节。

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

  • 使用 GCC 选项 -fno-asynchronous-unwind-tables 删除任何 .eh_frame 部分。在这种情况下,这就是在 DOS COM 程序末尾附加不需要的数据的原因
  • 使用 GCC 选项 -static 在不重定位的情况下构建以避免任何形式的动态链接。
  • 让 GCC 将 --build-id=none 选项传递给带有 -Wl 的链接器,以避免不必要地生成任何 .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 文件中的 space。我还将 *(.rodata); 更改为 *(.rodata*); 并将 *(.text); 更改为 *(.text*); 因为 GCC 可以生成以 .rodata.text 开头但具有不同后缀的节名称在他们身上。


内联汇编

与您询问的问题无关,但很重要。在此内联程序集中:

__asm__(
    "mov [=12=]x09, %%ah;"
    "int [=12=]x21;"
    : // no output
    : "d"(str)
    : "ah"
);

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

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

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

__asm__(
        ".code16gcc;"
        "call dosmain;"
        "mov [=13=]x4C, %AH;"
        "int [=13=]x21;"
);

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

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

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

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

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

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

如果您更改内联程序集以包含像这样的 "memory" 破坏程序:

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

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

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

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

00000118 <dosmain>:
 118:   66 57                   push   %edi
 11a:   66 56                   push   %esi
 11c:   66 83 ec 10             sub    [=16=]x10,%esp
 120:   67 66 8d 7c 24 03       lea    0x3(%esp),%edi
 126:   66 be 48 01 00 00       mov    [=16=]x148,%esi
 12c:   66 b9 0d 00 00 00       mov    [=16=]xd,%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    [=16=]x9,%ah
 13c:   cd 21                   int    [=16=]x21
 13e:   66 83 c4 10             add    [=16=]x10,%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 [=16=]x0,%al

内联汇编的替代版本,它使用变量通过 a 约束传递子函数 9 并将其标记为 input/output 和 +(因为 return AX 的值被破坏)可以这样完成:

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

建议:不要使用 GCC 生成 16 位代码。内联汇编是 difficult to get right and you will probably be using a fair amount of it for low level routines. You could look at Smaller C, Bruce's C compiler, or Openwatcom C 作为替代。都能生成DOS COM程序。

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

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

/DISCARD/ : 
{
     *(.eh_frame)
}

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