Open13

低レベルプログラミング 第2章

hirahira

問題11 xor, rdi, rdiとは、何をする命令だろうか?

2つの排他的論理和をとって第2引数のほうに書き込む。この場合、rdiには1が格納されているので1同士の排他的論理和、つまり0をrdiに書き込む。
(引数という表現が正しいのかわからないが…)

問題12 このプログラムのリターンコードは何だろう?

0

問題13 exitシステムコールの第1引数は?

リターンコード。rdiに格納される値が第1引数の値なので、この場合は0。問題12の答えが0なのはこのため。

hirahira

問題11 補足

mov rdi, 0と動作的には同じだが、movよりxorのほうが使用する容量が少なく、この用途ではxorを使うのが一般的とのこと。

hirahira

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

hirahira

問題14 上記のサンプルであげたASCIIコードが正しいことをチェックしよう

本では"4"が0x34、"a"が0x61だと挙げられていた。ASCIIコードはググれば出てくるが、正しさの担保という意味だとそこらへんのサイトでチェックするのは微妙。どこを見たらいいのかよくわからず。

以下のコマンドで確認できるとのこと。

man ascii
hirahira

問題15 sarとshrの違いは?Intelのドキュメントで調べよう。

Intelのドキュメント
https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html

このあたりか

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になる場合はどうなるんだと思ったけど、右シフトなんだからそんなことは起こらないね…

hirahira

問題16 数を、10進以外の方法で(ただしNASMが理解できるように)書くには、どうすればいいだろうか。NASMのドキュメントをチェックしよう。

NASMはアセンブリ言語の処理系、つまりアセンブラ。

NASMのドキュメント
https://www.nasm.us/docs.php

このへんかな

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

https://www.nasm.us/xdoc/2.16.03/html/nasmdoc3.html#section-3.4.1

思ったより多い。

hirahira

問題17 jeとjzの違いは?

jeは同じだったらジャンプする、jzはゼロだったらジャンプする、だが、いずれもゼロフラグが立っていたらジャンプするので動作的には同じ。

hirahira

リトルエンディアンについて

そもそもメモリは1つのセルにつき1バイトしか格納できないので、2バイト以上のデータを格納する際は分割して配置することになる。1バイトずつに分割したデータをメモリ上にどういう順番で並べるかのルールがエンディアンで、リトルエンディアンは2バイト以上のデータをメモリに格納する際、最下位バイトを先頭としてメモリに配置する。
本の例にある0x1122334455667788は、以下のように配置される。

0x88 0x77 0x66 0x55 0x44 0x33 0x22 0x11

ビッグエンディアンはこの逆になる。直感に反するほうがリトルエンディアンと雑に認識。

hirahira

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バイトのデータがどのように格納されるのかはわからんが、この話とは関係ないか。

hirahira

問題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バイトを読み込む場合はの話)

hirahira

問題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になっている場合もあるが)

hirahira

↑あとr13は呼び出し先退避レジスタなので、使う前に退避して、使ったあとに復元しなきゃいけないというのもあった。raxとかであれば退避しなくていい。

r13は退避しなきゃいけなくてraxは退避しなくていいというのは規約で決まっていることで、技術的な制約はない。

hirahira

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