writeシステムコールによる文字列の出力
writeシステムコール
下記は,「hello, world」と端末画面に出力するプログラムだ。 EAXに4を代入して int 0x80 を実行すると,writeシステムコールが呼び出される。 writeシステムコールの引数として,EBXにファイルディスクリプタ(出力先番号),ECXに出力文字列の先頭番地,EDXに文字列の長さを格納しておかないといけない。 標準出力のファイルディスクリプタは1だ。 writeシステムの実行が終わると,int 0x80の次の命令に復帰して実行を続ける。
section .text
global _start
_start:
mov eax, 4 ; writeのシステムコール番号
mov ebx, 1 ; 標準出力
mov ecx, msg ; 文字列の開始番地
mov edx, msglen ; 文字列の長さ
int 0x80
mov eax, 1 ; exitのシステムコール番号
mov ebx, 0 ; 終了コード
int 0x80
section .data
msg: db "hello, world", 0x0a
msglen: equ $ - msg ; 文字列の長さ (= この行の番地とmsgの差)
"hello, world"
のように二重引用符 "
(または単引用符 '
)で囲んだ記述は,各文字の文字コードからなるデータ列を表す。
つまり,ラベル msg の付いた行は,下記の記述と同じだ(ASCII文字コード表を使うコンピュータの場合)。
末尾の 0x0a (= 10) は「改行」の文字コードだ。
msg: db 104,101,108,108,111, 44,32,119,111,114, 108,100,10
出力文字列の生成
writeシステムコールは,引数の一つとして文字列の先頭番地を取ることに決まっているので,出力したい文字列は(仮に1文字しかなくても)主記憶の中に置かなければならない。
下記は,「9876543210
(改行)」という文字列を出力するプログラムだ。
この(改行を入れて11文字の)文字列をプログラム実行中に生成して,writeシステムコールを呼び出している。
; "9876543210" を出力
nchar: equ 10 ; 出力文字数
section .text
global _start
_start:
mov ecx, buf + nchar ; 作業領域の末尾の次の番地
mov edx, nchar ; 出力文字数
mov al, '0' ; 文字 0
; 領域 buf に "9876543210" を書き込む
loop0: dec ecx ; 次の書き込み先
mov [ecx], al ; 作業領域に1文字書き込む
inc al ; 次の文字
dec edx ; 残り文字数
jnz loop0 ; 残り文字数 > 0 ならループ
; writeシステムコールを呼び出す。そのあと終了
mov eax, 4 ; システムコール番号
mov ebx, 1 ; 標準出力
mov edx, nchar + 1 ; 改行を含めた長さ
int 0x80 ; writeシステムコール
mov eax, 1 ; システムコール番号
mov ebx, 0 ; 終了コード
int 0x80 ; exitシステムコール
section .data
buf: times nchar db 0 ; nchar文字分の領域
db 0x0a ; 改行
ラベル buf から始まる10文字分の領域に,出力したい文字列を書き込み,writeシステムコールを呼び出している。改行文字は予め buf + 10 番地の値として記述してある。
この例では領域の後ろから順に(文字 0
から順に),主記憶内領域に文字コードを書き込んでいる。
前から順に(文字 9
から順に)書き込んでも構わない。
writeシステムコールを呼び出す時点で出力すべき文字列が完成していればよい。
このプログラム例では,loop0から始まるループを抜けたとき,ECXが出力文字列の先頭を指しているので,これをそのままwriteシステムコールの引数の一つとして使っている。
'0'
のように文字を引用符で囲んで記述すると,その文字の文字コードを表す。
文字コード表を調べてその数値をプログラム中に書くよりも,'0'
のように書く方がよい。
楽だし,誤りを防止できるし,その数値の意味(0
の文字コードであること)がわかりやすい。
上記プログラムでは,0
, 1
, 2
, ... の文字コードが「連続」していること('1'
は'0'
+ 1に等しい。'2'
は'0'
+ 2に等しい。'9'
は'0'
+ 9に等しい)を利用している。
この性質を使うと,例えばレジスタに0〜9の数が格納されているとき,それに '0'
を加えれば,数字(文字)に変換できる。
練習問題
以下の問題で述べるプログラムを作りなさい。
リポジトリのルートディレクトリにサブディレクトリ chap4 を作成し,その中にソースファイルを置きなさい。
演習1.4-1 N という名前の名前付き定数を定義し,N の値を10進数で端末画面(正確には標準出力)に出力するアセンブリ言語プログラムを作りなさい。
N はEQU命令を使って定義しなさい。
N の値は 0 以上 232 未満とする。
出力は,0
〜9
の文字が1個以上並んだ後に改行文字が1個続くものとし,それ以外の文字は(表示可能文字以外も含めて)出力してはいけない。
先頭部分に余分な 0 があってもよい(例えば 123 の代わりに 0000000123 と出力してもよい)が,余分な 0 を出力しない方がより望ましい。
作成したプログラムを共有リポジトリにpushしなさい。
ただし,リポジトリ中にサブディレクトリ chap4 を作成し,その中にソースファイルを printdec.s という名前で作成すること。
(補足)「0
〜9
と改行以外は,表示可能文字以外も含めて出力してはいけない」ので,例えばヌル文字(文字コード 0 の文字)を出力することも仕様に反している。
演習1.4-2 N という名前の名前付き定数を定義し,N の値を16進数で端末画面(正確には標準出力)に出力するアセンブリ言語プログラムを作りなさい。
N はEQU命令を使って定義しなさい。
N の値は 0 以上 232 未満とする。
出力は,0
〜9
及びa
〜f
の文字がある個数並んだ後に改行文字が1個続くものとし,それ以外の文字は(表示可能文字以外も含めて)出力してはいけない。
32ビットの数を16進数で表すのに必要かつ十分な桁数を考え,どの N に対しても常に同じ桁数を出力すること。
ソースファイルを,サブディレクトリ chap4 に printhex.s という名前で作成すること。
作成したプログラムを共有リポジトリにpushしなさい。
ただし,演習1.4-1のプログラムをコミットしたメンバーとは別のメンバーがコミットすること(共有リポジトリ上のprintdec.sの最終変更者が,printhex.sの最終変更者と異なるようにすること)。
(ヒント)0
〜9
の文字コードが連続しているように,a
〜f
の文字コードも連続している('b'
は'a'
+ 1と等しい。'c'
は'a'
+ 2と等しい。'f'
は'a'
+ 5と等しい)。
ただし,9
とa
は連続していない('a'
≠ '9'
+ 1)。
従って,0〜15の数値を16進数字に変換したいときは,「10以上かどうか」で場合分けする必要がある。
演習1.4-3 フィボナッチ数 fn を,漸化式 f0 = 0,f1 = 1,fn = fn−1 + fn−2 (n ≧ 2) で定義する。以下の実行例のように, f2, f3, ..., f20 の値を順に,1行に一つずつ,10進数で出力するアセンブリ言語プログラムを作りなさい。
$ ./a.out
1
2
3
5
8
13
-- 中略 --
2584
4181
6765
出力の各行は,0
〜9
の文字が1個以上並んだ後に改行文字が1個続くものとし,それ以外の文字は(表示可能文字以外も含めて)出力してはいけない。
先頭部分に余分な 0 があってもよい(例えば 123 の代わりに 0000000123 と出力してもよい)が,余分な 0 を出力しない方がより望ましい。
ソースファイルを,サブディレクトリ chap4 に printfib.s という名前で作成すること。
作成したプログラムを共有リポジトリにpushしなさい。
ただし,3人グループの場合,演習1.4-1及び演習1.4-2のプログラムをコミットしたメンバーとは別のメンバーがコミットすること(共有リポジトリ上のprintfib.sの最終変更者が,printdec.sの最終変更者ともprinthex.sの最終変更者とも異なるようにすること)。2人グループの場合はどちらがコミットしても構わない。
(ヒント)「f2, ..., f20 をすべて計算してからそれらを順に出力する」方法や,「fnの計算とその出力を n = 2, 3, ..., 20 に対して行う」方法などが考えられる。前者の場合,32ビット数19個分の領域を確保して,fnの値を保存するために使えばよい。 後者の場合,演習1.3-1のプログラムと演習1.4-1のプログラムを一つのループの中に収めればよい。そのままだとおそらくレジスタが足りなくなるが,32ビットの主記憶内領域を2〜3個用意して,それらをレジスタの代わりに使えばよい。