gcc で DOS プログラミングしてみる


はじめに

@moriaiさんの書かれた『Rust で DOS プログラミングしてみる?』という記事を拝見し、いまどきのツールでリアルモードで動作するプログラムを作成をすることに興味を持ったのですが、Rust はなんもわからんので gcc で試してみることとしました。

環境

開発環境の OS やら gcc やらは以下の版のものを使用しています。

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=19.04
DISTRIB_CODENAME=disco
DISTRIB_DESCRIPTION="Ubuntu 19.04"
$ gcc --version
gcc (Ubuntu 8.3.0-6ubuntu1) 8.3.0
Copyright (C) 2018 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ as --version
GNU assembler (GNU Binutils for Ubuntu) 2.32
Copyright (C) 2019 Free Software Foundation, Inc.
This program is free software; you may redistribute it under the terms of
the GNU General Public License version 3 or later.
This program has absolutely no warranty.
This assembler was configured for a target of `x86_64-linux-gnu'.
$ ld --version
GNU ld (GNU Binutils for Ubuntu) 2.32
Copyright (C) 2019 Free Software Foundation, Inc.
This program is free software; you may redistribute it under the terms of
the GNU General Public License version 3 or (at your option) a later version.
This program has absolutely no warranty.
$ 

あと実行環境として、Windows10Home 64bit 上で MS-DOS Player for Win32-x64 の i486-x64 版を使用しています。

書いたコード

makefile
CC      = gcc
CFLAGS  = \
        -march=i386 -m16 \
        -std=c11 \
        -Wall -Wextra \
        -fno-pic \
        -fdata-sections -ffunction-sections \
        -fno-align-functions \
        -fno-builtin \
        -flto \
        -O2
AS      = $(CC)
AFLAGS  = $(CFLAGS)
LD      = gcc
LFLAGS  = \
        -march=i386 -m16 \
        -Tlink.ld \
        -nostdlib -static \
        -flto \
        -Wl,--gc-sections \
        -Wl,--build-id=none
PROG    = hello.com
PROGX   = $(PROG:.com=.x)
OBJS    = start.o hello.o doscall.o

.SUFFIXES:
.SUFFIXES: .c .S .o
.c.o:
        $(CC) $(CFLAGS) -c $< -o $@
.S.o:
        $(AS) $(AFLAGS) -c $< -o $@

target: $(PROG)
clean:
        rm -f $(OBJS) $(PROG) $(PROGX)

all: clean target

$(PROGX): $(OBJS)
        $(LD) $(LFLAGS) $^ -o $@ -Wl,-Map=$*.map

$(PROG): $(PROGX)
        objcopy $< -O binary $@

doscall.o: doscall.h
hello.o: doscall.h
link.ld
OUTPUT_FORMAT(elf32-i386)
ENTRY(_start)
SECTIONS
{
    . = 0x100;
    .text : {
        *(.start)
        *(.text) *(.text.*)
    }
    .data : {  
        *(.data) *(.data.*)
        *(.rodata) *(.rodata.*)
        *(.gcc_except_table)
    }
    .bss (NOLOAD) : {
        *(.bss) *(.bss.*)  
        *(COMMON)
    }
    /DISCARD/ : { *(.*) }  
}
start.S
        .code16gcc  
        .text
        .section ".start", "ax"
        .globl  _start
_start:
        call   main
        .code16
        ret
        .end
doscall.h
#ifndef DOSCALL_H
#define DOSCALL_H

typedef unsigned char uint8_t;
typedef unsigned short uint16_t;

void dos_write(uint16_t fd, const void* buf, uint16_t count);
_Noreturn void dos_exit(uint8_t status);

#endif/*DOSCALL_H*/
doscall.c
#include "doscall.h"

void dos_write(uint16_t fd, const void* buf, uint16_t count)
{
    __asm __volatile(
        "mov $0x40, %%ah \n"
        "int $0x21 \n"
        :
        : "b"(fd), "c"(count), "d"(buf)
        : "eax"
    );
}

_Noreturn void dos_exit(uint8_t status)
{
    __asm __volatile(
        "mov $0x4c, %%ah \n"
        "int $0x21 \n"
        :
        : "a"(status)
    );
    __builtin_unreachable();
}
hello.c

int main(void)
{
    const uint16_t stdout = 1;
    static const char __attribute__((aligned(1))) msg[14] = "Hello, World\r\n";

    dos_write(stdout, msg, sizeof(msg));
    dos_exit(0);
}       

これを上記環境で make することで 52バイトの hello.com が作成されました。

$ make
gcc -march=i386 -m16 -std=c11 -Wall -Wextra -fno-pic -fdata-sections -ffunction-sections -fno-align-functions -fno-builtin -flto -O2 -c start.S -o start.o
gcc -march=i386 -m16 -std=c11 -Wall -Wextra -fno-pic -fdata-sections -ffunction-sections -fno-align-functions -fno-builtin -flto -O2 -c hello.c -o hello.o
gcc -march=i386 -m16 -std=c11 -Wall -Wextra -fno-pic -fdata-sections -ffunction-sections -fno-align-functions -fno-builtin -flto -O2 -c doscall.c -o doscall.o
gcc -march=i386 -m16 -Tlink.ld -nostdlib -static -flto -Wl,--gc-sections -Wl,--build-id=none start.o hello.o doscall.o -o hello.x -Wl,-Map=.map 
objcopy hello.x -O binary hello.com
$ ls -l hello.com
-rwxr-xr-x 1 fujita fujita 52 Jan 12 00:00 hello.com
$

MS-DOS Player を使用して動作させてみると

$ msdos hello.com
Hello, World

$

動作しました。

コード評価

hello.com の内容を見てみると

$ hexdump -C hello.com
00000000  66 e8 01 00 00 00 c3 66  53 66 bb 01 00 00 00 66  |f......fSf.....f|
00000010  b9 0e 00 00 00 66 ba 26  01 00 00 b4 40 cd 21 66  |.....f.&....@.!f|
00000020  31 c0 b4 4c cd 21 48 65  6c 6c 6f 2c 20 57 6f 72  |1..L.!Hello, Wor|
00000030  6c 64 0d 0a                                       |ld..|
00000034
$ objdump -b elf32-i386 -m i8086 -d hello.x

hello.x:     file format elf32-i386


Disassembly of section .text:

00000100 <_start>:
 100:   66 e8 01 00 00 00       calll  107 <main>
 106:   c3                      ret    

00000107 <main>:
 107:   66 53                   push   %ebx
 109:   66 bb 01 00 00 00       mov    $0x1,%ebx
 10f:   66 b9 0e 00 00 00       mov    $0xe,%ecx
 115:   66 ba 26 01 00 00       mov    $0x126,%edx
 11b:   b4 40                   mov    $0x40,%ah
 11d:   cd 21                   int    $0x21
 11f:   66 31 c0                xor    %eax,%eax
 122:   b4 4c                   mov    $0x4c,%ah
 124:   cd 21                   int    $0x21
$

無駄な push %ebx が生成されてしまっている点は残念です。また、システムコールのパラメタの設定に 16ビットレジスタで済むところを 32ビットレジスタを使用してしまい冗長なコードとなってしまっている印象です。リンク時最適化が働いてシステムコールの関数呼び出しがインライン展開されてる点は良い感じですね。
いまどきはリアルモード用のプログラムを書く機会もそうそうないとは思いますが、OS のブートローダーを書いたりする程度の用途には使えるかもしれません。

おわりに

おわりです。