低レベルプログラミング 第2章
問題11 xor, rdi, rdiとは、何をする命令だろうか?
2つの排他的論理和をとって第2引数のほうに書き込む。この場合、rdiには1が格納されているので1同士の排他的論理和、つまり0をrdiに書き込む。
(引数という表現が正しいのかわからないが…)
問題12 このプログラムのリターンコードは何だろう?
0
問題13 exitシステムコールの第1引数は?
リターンコード。rdiに格納される値が第1引数の値なので、この場合は0。問題12の答えが0なのはこのため。
問題11 補足
mov rdi, 0
と動作的には同じだが、movよりxorのほうが使用する容量が少なく、この用途ではxorを使うのが一般的とのこと。
2.3 例: レジスタの内容を出力する
自分なりの理解でコメントをつけた。
section .data
codes:
db '0123456789ABCDEF'
section .text
global _start
_start:
; 出力する値
mov rax, 0x1122334455667788
; 出力先の設定(標準出力)
mov rdi, 1
; 出力するバイト数
mov rdx, 1
; ループカウンタ。下で4ずつ引いているので16回ループが回る
mov rcx, 64
.loop:
push rax
sub rcx, 4
; clは4ずつ減っていくので、raxから取り出すビット数は逆に4ずつ増えていく
sar rax, cl
; raxの下位4ビットを抽出する
; 4ビットを増やしてから下位4ビット抽出ということは、つまり取得する値を4ビットずつずらしている
; この時点のraxの値は、もとのraxを16進数文字列と考えたとき、左から見てループ回数文字目の数値となる
and rax, 0xf
; メモリアドレスを計算してrsiに設定(rsiはwriteシステムコールで書き込み対象の値のメモリアドレスの先頭)
; これによって、raxが1なら'1'、4なら'4'、15なら'F'、のように書き込める
lea rsi, [codes + rax]
; writeシステムコールを示す1
mov rax, 1
; システムコールはrcxを書き換えるので退避
push rcx
syscall
pop rcx
; ここで取り出すのはraxの初期値(出力する値)
pop rax
; rcxがゼロならゼロフラグを立てて、ゼロフラグが立っていなかったらジャンプ
; つまり所定の回数ループして、それが終わったら抜けるための記述
test rcx, rcx
jnz .loop
; exitシステムコール
mov rax, 60
xor rdi, rdi
syscall
問題14 上記のサンプルであげたASCIIコードが正しいことをチェックしよう
本では"4"が0x34、"a"が0x61だと挙げられていた。ASCIIコードはググれば出てくるが、正しさの担保という意味だとそこらへんのサイトでチェックするのは微妙。どこを見たらいいのかよくわからず。
以下のコマンドで確認できるとのこと。
man ascii
問題15 sarとshrの違いは?Intelのドキュメントで調べよう。
Intelのドキュメント
このあたりか
The SHR instruction clears the most significant bit (see Figure 7-8 in the Intel® 64 and IA-32 Architectures Software Developer’s Manual, Volume 1);
the SAR instruction sets or clears the most significant bit to correspond to the sign (most significant bit) of the original value in the destination operand.
どちらも右ビットシフトするのは同じだが、最上位ビット(符号あり整数の場合は符号ビット)の扱いに違いがある。
sarは最上位ビットに元の値を設定する。shrは最上位ビットに0を設定する。
結果としてsarは符号あり整数を扱うために使用、shrは符号なし整数を扱うために使用する。
符号ビットを固定で0にしてしまうとなると、普通に値として最上位ビットが1になる場合はどうなるんだと思ったけど、右シフトなんだからそんなことは起こらないね…
問題16 数を、10進以外の方法で(ただしNASMが理解できるように)書くには、どうすればいいだろうか。NASMのドキュメントをチェックしよう。
NASMはアセンブリ言語の処理系、つまりアセンブラ。
NASMのドキュメント
このへんかな
A numeric constant is simply a number. NASM allows you to specify numbers in a variety of number bases, in a variety of ways: you can suffix H or X, D or T, Q or O, and B or Y for hexadecimal, decimal, octal and binary respectively, or you can prefix 0x, for hexadecimal in the style of C, or you can prefix $ for hexadecimal in the style of Borland Pascal or Motorola Assemblers. Note, though, that the $ prefix does double duty as a prefix on identifiers (see section 3.1), so a hex number prefixed with a $ sign must have a digit after the $ rather than a letter. In addition, current versions of NASM accept the prefix 0h for hexadecimal, 0d or 0t for decimal, 0o or 0q for octal, and 0b or 0y for binary. Please note that unlike C, a 0 prefix by itself does not imply an octal constant!
mov ax,200 ; decimal
mov ax,0200 ; still decimal
mov ax,0200d ; explicitly decimal
mov ax,0d200 ; also decimal
mov ax,0c8h ; hex
mov ax,$0c8 ; hex again: the 0 is required
mov ax,0xc8 ; hex yet again
mov ax,0hc8 ; still hex
mov ax,310q ; octal
mov ax,310o ; octal again
mov ax,0o310 ; octal yet again
mov ax,0q310 ; octal yet again
mov ax,11001000b ; binary
mov ax,1100_1000b ; same binary constant
mov ax,1100_1000y ; same binary constant once more
mov ax,0b1100_1000 ; same binary constant yet again
mov ax,0y1100_1000 ; same binary constant yet again
思ったより多い。
問題17 jeとjzの違いは?
jeは同じだったらジャンプする、jzはゼロだったらジャンプする、だが、いずれもゼロフラグが立っていたらジャンプするので動作的には同じ。
リトルエンディアンについて
そもそもメモリは1つのセルにつき1バイトしか格納できないので、2バイト以上のデータを格納する際は分割して配置することになる。1バイトずつに分割したデータをメモリ上にどういう順番で並べるかのルールがエンディアンで、リトルエンディアンは2バイト以上のデータをメモリに格納する際、最下位バイトを先頭としてメモリに配置する。
本の例にある0x1122334455667788は、以下のように配置される。
0x88 0x77 0x66 0x55 0x44 0x33 0x22 0x11
ビッグエンディアンはこの逆になる。直感に反するほうがリトルエンディアンと雑に認識。
2.5.1 エンディアン
demo1: dq 0x1122334455667788
demo2: db 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88
これらについて本のプログラムで出力結果が異なる理由はメモリ配置の順番がそもそも異なるから。
読みとる命令の記述は同じなので。
mov rdi, [demo1]
mov rdi, [demo2]
rdi
は8バイトのサイズを持つレジスタなので、上記のいずれも連続する8バイトが読み取られる。リトルエンディアンだと最下位バイトから順にメモリに格納されることから、複数バイトのデータを一気に読み取る場合は整合性がとれるように最下位バイトから順に読むようになっているのかと思われる。
rdi
の内部で8バイトのデータがどのように格納されるのかはわからんが、この話とは関係ないか。
問題18 上にあげたリストにある4個のコマンドを、それぞれ実行した結果、testの値はどうなるか?
こうなると思ったのだが
mov byte[test], 1 ; test: FF FF FF FF FF FF FF 01
mov word[test], 1 ; test: FF FF FF FF FF FF 00 01
mov dword[test], 1 ; test: FF FF FF FF 00 00 00 01
mov qword[test], 1 ; test: 00 00 00 00 00 00 00 01
実際にはこうだった。リトルエンディアンだからそれはそう。
mov byte[test], 1 ; test: 01 FF FF FF FF FF FF FF
mov word[test], 1 ; test: 01 00 FF FF FF FF FF FF
mov dword[test], 1 ; test: 01 00 00 00 FF FF FF FF
mov qword[test], 1 ; test: 01 00 00 00 00 00 00 00
読み取られる時のイメージとしては上の認識が近いのかもしれないが(何バイト読み取るのかは命令によって異なるので、あくまでも8バイトを読み込む場合はの話)
問題19 次に示すリスト2-15のバグを指摘せよ。
global _start
section .data
test_string: db "abcdef", 0
section .text
strlen:
.loop:
cmp byte [rdi+r13], 0
je .end
inc r13
jmp .loop
.end:
mov rax, r13
ret
_start:
mov rdi, test_string
call strlen
mov rdi, rax
mov rax, 60
syscall
r13レジスタの初期値が0であることを期待しているが、0で初期化していないので実際には0になっているとは限らない。(0になっている場合もあるが)
↑あとr13は呼び出し先退避レジスタなので、使う前に退避して、使ったあとに復元しなきゃいけないというのもあった。raxとかであれば退避しなくていい。
r13は退避しなきゃいけなくてraxは退避しなくていいというのは規約で決まっていることで、技術的な制約はない。
2.7 課題: 入出力ライブラリ
難しすぎて挫折… できた分だけ書いた。
section .text
exit:
mov rax, 60
syscall
string_length:
; raxを0で初期化
xor rax, rax
.loop:
; NULL文字かどうか判定
; rdiは文字列の先頭アドレス(第一引数)
cmp byte [rdi+rax], 0
; NULL文字なら終端なので抜ける
je .end
; NULL文字じゃなかったので次の文字を見るようにraxをインクリメント
inc rax
; ループ
jmp .loop
.end:
; raxが戻り値
; 0-indexedなのでデクリメントはしなくていい
ret
print_string:
; 関数を実行。引数はrdiに入っているのでそのまま
call string_length
; rax, rdiはあとで使うので適当な汎用レジスタ入れる
mov r8, rax
mov r9, rdi
; writeシステムコール
mov rax, 1
; 標準出力
mov rdi, 1
; 出力対象の先頭アドレス
mov rsi, r9
; バイト数
mov rdx, r8
syscall
ret
print_char:
; 出力対象の値はメモリに配置するしかない
; ASCIIであれば1バイトで、rdiのサイズは8バイトで、残り7バイトは0で埋められているはず。その0がNULL文字として作用する
; 1文字8バイト未満の文字なら問題ないはず
push rdi
; 引数
; rspはスタックの先頭アドレス
mov rdi, rsp
call print_string
ret
print_newline:
mov rdi, 0x0A
call print_char
ret
print_uint:
; 被除数をraxに
mov rax, rdi
; 除数
mov r8, 10
; ASCIIコードに変換するためのオフセット
mov r9, 48
; バッファを用意
sub rsp, 64
lea rsi, [rsp + 63]
; NULL文字
mov byte [rsp + 64], 0
.loop:
; rdxを0でクリアしないと結果が不正になるらしい
xor rdx, rdx
; 10進数1桁を取り出すための除算
; 商がrax、剰余がrdxに入る
div r8
; 48を足すとASCIIコードになる
mov r10, rdx
add r10, r9
; メモリに配置
; r10bはr10の最小バイトで、ここにASCIIコードが入っている。メモリ上に連続させたいので1バイト分だけ書き込む意図
mov [rsi], r10b
dec rsi
; ループを抜けるかどうか判定
cmp rax, 0
; ゼロフラグが立っていたら、つまりraxが0だったら抜ける
jz .exit
jmp .loop
.exit:
; print
; rsiは先頭より1つ前を指しているので+1
lea rdi, [rsi + 1]
call print_string
ret
print_int:
; 負の数でないならprint_uint
test rdi, rdi
js .negative
call print_uint
jmp .exit
.negative:
; 被除数をraxに
mov rax, rdi
; 除数
mov r8, 10
; ASCIIコードに変換するためのオフセット
mov r9, 48
; バッファを用意
sub rsp, 64
lea rsi, [rsp + 63]
; NULL文字
mov byte [rsp + 64], 0
.loop:
cqo
; 10進数1桁を取り出すための除算
; 商がrax、剰余がrdxに入る
idiv r8
; 符号を反転
neg rdx
; 48を足すとASCIIコードになる
mov r10, rdx
add r10, r9
; メモリに配置
; r10bはr10の最小バイトで、ここにASCIIコードが入っている。メモリ上に連続させたいので1バイト分だけ書き込む意図
mov [rsi], r10b
dec rsi
; ループを抜けるかどうか判定
cmp rax, 0
; ゼロフラグが立っていたら、つまりraxが0だったら抜ける
jz .exit
jmp .loop
.exit:
; 符号をつける
mov byte [rsi], '-'
; print
lea rdi, [rsi]
call print_string
ret
string_equals:
xor rax, rax
ret
read_char:
; スタック上にバッファを設ける
dec rsp
; readシステムコール
mov rax, 0
; 標準入力
mov rdi, 0
; バッファ
mov rsi, rsp
; 読み取るバイト数
mov rdx, 1
syscall
; 読み取ったバイト数がraxに入る。これが0ならEOF
cmp rax, 0
je .eof
; 戻り値
mov al, [rsp]
; スタックを戻す
add rsp, 1
ret
.eof:
; 戻り値
xor rax, rax
; スタックを戻す
add rsp, 1
ret
read_word:
push r14
push r15
; rdi, rsiで引数を受け取っている
; 読み取った文字数
xor r14, r14
; バッファサイズ - 1
mov r15, rsi
dec r15
; 最初の空白を検出するためのループ
.loop:
; 読み取り
push rdi
call read_char
pop rdi
; 空白文字チェック
; スペース
cmp al, ' '
je .loop
; 水平タブ
cmp al, 9
je .loop
; LF
cmp al, 10
je .loop
; CR
cmp al, 13
je .loop
; 0だったら抜ける
cmp rax, 0
je .end
; read_charで1文字ずつ読み取る
; ループして、0が返ってくるまで読み続ける
; バッファサイズを超えないかどうかの判定のため、読み取った文字数を記録していく
.read_rest_loop:
; 初回は上のread_charで空白文字以外が読まれているはず
; バッファに格納
; alはraxの最下位バイト
mov byte [rdi+r14], al
; 文字数をインクリメント
inc r14
; 読み取り
push rdi
call read_char
pop rdi
; 0だったら抜ける
cmp rax, 0
je .end
; 空白文字なら単語の終わりとして抜ける
; スペース
cmp al, ' '
je .end
; 水平タブ
cmp al, 9
je .end
; LF
cmp al, 10
je .end
; CR
cmp al, 13
je .end
; バッファサイズのチェック
cmp r14, r15
jae .buffer_overflow
jmp .read_rest_loop
.end:
; NULL文字
mov byte [rdi+r14], 0
; 戻り値のバッファのアドレスって引数をそのまま返す
; 1つ目の戻り値はraxに、2つ目はrdxに
mov rax, rdi
mov rdx, r14
pop r15
pop r14
ret
.buffer_overflow:
; バッファサイズ超過の場合は0を返す
xor rax, rax
pop r15
pop r14
ret