GNU asはGNU binutilsに含まれているアセンブラです。
本記事では、Assembler/ForFun(x86_32)/04, 16bit BIOS with NASM で作成したHello, Worldプログラムを GNU as 版に書き直してみます。
参考:
最初に INT10h - AH=13h を使った"Hello, World!"をGNU as版に書き直してみます。次の点に注意します。
拡張子は".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!"が改行処理されて表示されます。
今度はビデオメモリに直接書き込むバージョンを 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つの「ばらつき」、バリエーションについて簡単に紹介します。
"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) }
}
この違いはどこから来るかというと、本記事のリンカスクリプトでは
などの理由で4行に縮めています。
本記事のサンプルはどちらのリンカスクリプトでも動きますので、好きな方を使って下さい。
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" シンボルを公開してリンクしたい場合は次のようになるでしょう。
.text
.global main
main:
...
".global" と ".globl" の二種類があるのは他のアセンブラとの互換性確保のためです。「".global"ではなく".globl"を使え」と書いている資料もありますが、少なくとも2010年9月時点でのbinutils-2.20では、どちらを使っても変わりないようです。
本記事では一つのオブジェクトファイルだけでリンクしているため、この指示子は使っていません。
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版に書き直してみました。
また、記述にばらつきのある点について間単にまとめました。