home ホーム search 検索 -  login ログイン  | reload edit datainfo version cmd icon diff delete  | help ヘルプ

Assembler/ForFun(x86_32)/05, 16bit BIOS with GNU as

Assembler/ForFun(x86_32)/05, 16bit BIOS with GNU as

Assembler / ForFun(x86_32) / 05, 16bit BIOS with GNU as
id: 787 所有者: msakamoto-sf    作成日: 2010-09-18 18:24:41
カテゴリ: Assembler 

GNU asはGNU binutilsに含まれているアセンブラです。
本記事では、Assembler/ForFun(x86_32)/04, 16bit BIOS with NASM で作成したHello, Worldプログラムを GNU as 版に書き直してみます。

参考:



"Hello, World!" : INT 10h - AH=13h 版

最初に INT10h - AH=13h を使った"Hello, World!"をGNU as版に書き直してみます。次の点に注意します。

  • 16bitなので ".code16" 指示子を指定する。
  • GNU as の場合は AT&T の記法、つまり source, destination の順に書く。
  • レジスタの先頭は"%"、即値の先頭は"$"。
  • NASMでの"$"(現在アドレス)はGNU asの場合は"."(ドット)
  • "text", "data"などの主要セクションについては".text", ".data" 指示子を使う。
    • それ以外については ".section セクション名" 指示子で指定
  • コメントは "/*" - "*/" のC言語スタイル

拡張子は".s"にしておきます。gccのツールチェインで、Cプリプロセッサ(cpp)を通したい場合は大文字の".S"にする場合もあるようです(例:Linux Kernel)。拡張子とプリプロセッサの詳細は下記ドキュメントを参照して下さい。

hello1.s:

.code16
.text
    jmp $0x07C0, $mystart
mystart:
    /* copy CS(0x07C0) to DS, ES */
    mov %cs, %ax
    mov %ax, %ds
    mov %ax, %es

    /* get current video mode */
    xor %ax, %ax
    xor %bx, %bx
    mov $0x0f, %ah
    int $0x10
    /* active page num => BH */

    /* get cursor position and size */
    mov $0x03, %ah
    int $0x10
    /* (row, column) => (DH, DL) */

    /* write string */
    mov $0x13, %ah
    mov $0x1, %al  /* bit0 = 1 => update cursor */
    /* BH = page num, already set by INT10H/AH=0FH */
    mov $0b11111001, %bl /* blinking, bg=light gray, fg=light blue */
    mov hellomsg_len, %cx /* string length */
    /* DH, DL (row, column) already set by INT10H/AH=03H */
    push %bp
    mov $hellomsg, %bp
    int $0x10
    pop %bp

    jmp .

.data
hellomsg: .string "Hello, \r\nBIOS World!\r\n"
hellomsg_len: .word (. - hellomsg - 1)
/* ↑ ".string"で自動的に終端"0"が付くので、それを除去する為 -1 している */

コンパイル:

$ as -o hello1.o hello1.s

この時点ではELFオブジェクトファイルです。
専用のリンカスクリプトを使って、MBRとしてロード出来るフォーマットにリンクします。
リンクには同じくGNU binutilsに含まれる GNU ld を使います。GNU ld およびリンカスクリプトの詳細は下記リンクを参照して下さい。

今回は以下のリンカスクリプトを使います。
mbr.txt:

SECTIONS {
        .text : { *(.text) }
        .data : { *(.data) }
        .mbrsignature 510 : { SHORT(0xAA55) }
}

リンクしてみます。

$ ld -o hello1.img --oformat binary -T mbr.txt hello1.o

ところで、hello1.oをobjdumpで逆アセンブルしてみると、結果が少しおかしくなります。

$ objdump -d hello1.o

hello1.o:     file format elf32-i386

Disassembly of section .text:

00000000 <mystart-0x5>:
   0:   ea 05 00 c0 07 8c c8    ljmp   $0xc88c,$0x7c00005
                       ^^^^^ ????
00000005 <mystart>:
   5:   8c c8                   movl   %cs,%eax
                                           ^^^^^ EAX ?
...

これは32bitとして逆アセンブルしている為です。レジスタ名も32bit拡張されたEAXが表示されています。
16bitとして逆アセンブル表示するなら、 "-M, --disassembler-options" オプションで"data16"を指定します。

$ objdump -M data16 -d hello1.o

hello1.o:     file format elf32-i386

Disassembly of section .text:

00000000 <mystart-0x5>:
   0:   ea 05 00 c0 07          ljmp   $0x7c0,$0x5

00000005 <mystart>:
   5:   8c c8                   movl   %cs,%ax
   7:   8e d8                   movl   %ax,%ds
...

今度は大丈夫です。

"-M, --disassembler-options"の詳細については下記ドキュメントを参照して下さい。

さて、リンクで生成されたhello1.imgと Assembler/ForFun(x86_32)/04, 16bit BIOS with NASM で使ったhello.bxrcファイルを使ってBochsを起動します。
結果の画面スクリーンショットは省略しますが、NASMの時と同様に、"Hello, BIOS World!"が改行処理されて表示されます。

"Hello, World!" : ビデオメモリに直接書き込む版

今度はビデオメモリに直接書き込むバージョンを GNU as で書き直してみます。

hello2.s:

.code16
.text
    jmp $0x07C0, $mystart

videoseg = 0xB800
cols = 80
rows = 25
bgcolor = 0b01110000 /* no blink, bg = light gray, fg = black */
bgtext = 0x20 /* space char */

mystart:
    /* copy CS(0x07C0) to DS, ES */
    mov %cs, %ax
    mov %ax, %ds

    mov $videoseg, %ax
    mov %ax, %es
    mov $0, %di

    /* clear background color and texts */
    mov $bgtext, %al
    mov $bgcolor, %ah
    /* for (i = 0; i < rows; i++) { */
    mov $rows, %cx
_bg_fill_rows:
    push %cx
    /*     for (j = 0; j < cols; j++) { */
    mov $cols, %cx
_bg_fill_cols:
    mov %ax, %es:(%di)
    add $2, %di
    loop _bg_fill_cols
    /*     } */
    pop %cx
    loop _bg_fill_rows
    /* } */

    xor %ax, %ax
    xor %di, %di
    mov $hellomsg, %si
_print:
    movb %ds:(%si), %al
    cmp $0, %ax
    jz _print_end
    movb %al, %es:(%di)
    inc %si
    add $2, %di
    jmp _print
_print_end:

    jmp .

.data
hellomsg: .string "Hello, BIOS World!"

コンパイル+リンク:

$ as -o hello2.o hello2.s
$ ld -o hello2.img --oformat binary -T mbr.txt hello2.o

結果の画面スクリーンショットは省略しますが、NASMの時と同様に、灰色で塗りつぶされた画面の左上に"Hello, BIOS World!"が黒字で表示されます。

書き方のバリエーション

GNU as や GNU ld のドキュメントや記事を調べていくと、サンプルコードの書き方に「ばらつき」がある事に気づくと思います。
ここでは本記事と絡めて5つの「ばらつき」、バリエーションについて簡単に紹介します。

MBR用のリンカスクリプトの書き方について

"ForFun(x86_32)"シリーズの元ネタとなっている「アセンブリ言語の教科書」では、GNU asを使った16bitBIOSプログラミングの解説で次のような10行前後のリンカスクリプトを載せています。

OUTPUT_FORMAT("binary");
IPLBASE = 0x0000;

SECTIONS {
    . = IPLBASE;
    .text : { *(.text) }
    ...
    . = IPLBASE + 510;
    .sign : { SHORT(0xAA55) }
}

一方、本記事では僅か4行です。

SECTIONS {
        .text : { *(.text) }
        .data : { *(.data) }
        .mbrsignature 510 : { SHORT(0xAA55) }
}

この違いはどこから来るかというと、本記事のリンカスクリプトでは

  1. 出力フォーマットについては "--oformat binary" で指定するので、リンカスクリプトからは省いた。
  2. location counter (リンカスクリプト中での ".") は省略時は0から始まるので、わざわざIPLBASEを定義してSECTIONSの先頭で代入する必要は無いと判断し、省いた。
  3. ". = IPLBASE + 510"については、セクションの後ろに開始アドレスを指定する機能があるので、そちらを使って".mbrsignature 510 : ..." としたため、省いた。
  4. 本記事では ".text" と ".data" セクションしか使わない為、他のセクションの記述は省いた。

などの理由で4行に縮めています。

本記事のサンプルはどちらのリンカスクリプトでも動きますので、好きな方を使って下さい。

".code16" と ".code16gcc" の違い

x86で16bitコードを出力したい場合、".code16"と".code16gcc"の二種類の指示子のどちらかを指定します。
本記事では".code16"を指定しています。なぜ".code16gcc"を使わなかったかというと、".code16gcc"の場合はcall, ret, push, pop命令などが32bitサイズに変換されてしまう為です。全て16bit世界に収める為、本記事では".code16gcc"ではなく".code16"の方を使っています。

詳しくは本家記事を参照して下さい。

エントリポイントの指定

ld に "-e エントリポイント" でエントリポイントを指定するよう書かれているGNU as入門記事を見つける事があるかもしれません。
ldでは、"-e"オプションの他にリンカスクリプトで

ENTRY(symbol)

としてもエントリポイントを指定出来ます。

以下の本家ドキュメントに、どのようにエントリポイントが決定されるのか記述されていますので、もし分からなくなった時はこちらを参照して下さい。

本記事ではMBR領域をプログラミングしている為、単純に0番地から始まればOKです。よって、特に "-e"オプションやリンカスクリプトの"ENTRY()"コマンドでエントリポイントとなるシンボルは指定していません。その場合、"start"シンボルか".text"セクションの先頭かアドレス0がエントリポイントに決定されます。本記事のソースでは"start"シンボルは使っていないので、結局、".text"セクションの先頭がエントリポイントに決定され、MBR領域ですのでそれで問題有りません。

".global" or ".globl" を "main" などに指定する。

複数のオブジェクトファイルをリンクする時、他のオブジェクトファイルに対して公開するシンボルは ".global" or ".globl" 指示子を指定します。
例えば "main" シンボルを公開してリンクしたい場合は次のようになるでしょう。

.text
.global main
main:
    ...

".global" と ".globl" の二種類があるのは他のアセンブラとの互換性確保のためです。「".global"ではなく".globl"を使え」と書いている資料もありますが、少なくとも2010年9月時点でのbinutils-2.20では、どちらを使っても変わりないようです。

本記事では一つのオブジェクトファイルだけでリンクしているため、この指示子は使っていません。

bus error を回避する為、alignmentを指定する。

x86ではあまり遭遇しませんが、他のCPUで時々発生するbus errorを回避する為、データブロックのお尻を調節するための ".align" 指示子を指定する場合があります。

...
.text
.align 16
...
.data
.align 4
...
.section _foo ; .align 8 /* line separatorは改行 or ";" */
...

文字列データを記述する場合などは特に ".align" に注意する必要があるでしょう。

本記事では x86_32 のみを対象としていること、および実際に動かしてみてbus errorが発生していないため、".align"指示子は省略しています。

まとめ

Assembler/ForFun(x86_32)/04, 16bit BIOS with NASM で作成した2種類のNASM版"Hello, World!"をGNU as版に書き直してみました。
また、記述にばらつきのある点について間単にまとめました。



プレーンテキスト形式でダウンロード
現在のバージョン : 1
更新者: msakamoto-sf
更新日: 2010-09-19 20:58:57
md5:4559637f65a260d59c50389caae662b8
sha1:902be63ec322752d1792045de937c102d331c08c
コメント
コメントを投稿するにはログインして下さい。