Armの開発環境構築(Windows篇)
手順
WSLとWindows TerminalをMicrosoft Storeからインストール(Windows Terminalは必須ではないが、操作性に優れているため推奨)。各種設定は以下を参考に。
上記設定が完了したらWSLを起動し、Armのクロスコンパイラとユーザーモード用のQEMU(エミュレータ)をインストール。
$ sudo apt install gcc-arm-linux-gnueabi qemu-user
実行
とりあえずHello, World。hello.s
とでも名前を付けて保存しておこう。編集するテキストエディタはVimでもEmacsでもお好きなものを。
.text .global main main: push {lr} ldr r0, =msg bl printf mov r0, #0 @ good return code pop {pc} .data msg: .asciz "Hello, World!\n"
そして実行。
$ arm-linux-gnueabi-gcc hello.s $ qemu-arm -L /usr/arm-linux-gnueabi/ a.out Hello, World!
ちゃんとHello, World!
と表示されていれば成功。
hookコードでレジスタ書き出し(CTRPF)
概要
きっかけはどぅーむーんさんのツイート。
全レジスタ確認の考えまくったところ、可能性は低いがある。
— どぅーむーん (@domoon_mutex) 2021年3月14日
それも構成自体は超簡単に作れそうと考えれるから後は実現だけ。
内容としては、hookコードでレジスタ確認を行うというもの。実装は少々面倒かもしれないが、面白そうだったので組んでみることにした。(十分なテストを行っていないため、間違い等あればコメント欄まで)
手順
以下の手順で実装する。今回の方法ではlrとpcを参照できないため、確認するレジスタはr0~r13(sp)とする。
- r0~r13, cpsrをスタックに退避。
- スタックから値を読み込み、実行コードの下部に書き込む。
- 上書きしたレジスタを復元し、スタックポインタを戻す。
※hook処理にもスタックは使われる。spはあくまでhookコード開始時の値であり、hookアドレス時点での値ではないため注意。
コード
上記の手順に沿って実装。ちなみに各レジスタの役割は以下のように割り振った。
- r0 ・・・cpsrの格納と復元及び偶数番目のレジスタのロード
- r1・・・奇数番目のレジスタのロード
- r2・・・ロードするレジスタのポインタ
- r3・・・書き込み先のポインタ
- r4・・・push前のスタックポインタの値
E92D3FFF @ push {r0-r13} E10F0000 @ mrs r0, cpsr E52D0004 @ push {r0} E28D2004 @ add r2, sp, #4 E28F3028 @ add r3, pc, #0x28 E28D403C @ add r4, sp, #0x3C E0C200D8 @ ldrd r0, [r2], #8 E0C300F8 @ strd r0, [r3], #8 E1520004 @ cmp r2, r4 BAFFFFFB @ blt #-12 E49D0004 @ pop {r0} E129F000 @ msr cpsr, r0 E8BD001F @ pop {r0-r4} E28DD024 @ add sp, sp, #0x24 E12FFF1E @ bx lr
以下各コード解説。(コードをクリックして詳細確認)
push {r0-r13}
確認対象とするレジスタをスタックに積む。
mrs r0, cpsr
cpsrの値をr0にコピー。
push {r0}
r0(中身はcpsrの値)をスタックに積む。
add r2, sp, #4
r2に読み込みを開始するレジスタの位置を読み込む。この時r2はスタックに退避したr0の位置を指している。
add r3, pc, #0x28
r3に書き込み開始位置を読み込む。今回はhookコード終了命令(bx lr)の8byte先を指している。
add r4, sp, #0x3C
r4にpush前のspの位置を読み込む。
cmp r2, r4
r2とr4を比較。r2は次のロード開始位置、r4は元のスタックポインタの位置を指している。つまり、「ロードすべきレジスタが残っているか」を判断している。
pop {r0}
スタックに積んでいたcpsrの値をr0に読み込む。
msr cpsr, r0
r0の値をcpsrにコピー。これで元のcpsrの値が復元される。
pop {r0-r4}
処理に使用したレジスタ(r0~r4)は復元する必要があるため、スタックから復元する。
add sp, sp, #0x24
スタックポインタを元の位置に戻す。
bx lr
lrに分岐し、hookコード終了。
D3000000 XXXXXXXX // フックするアドレス FD000000 00000078 // フック開始(0x78 byte) E92D3FFF E10F0000 E52D0004 E28D2004 E28F3028 E28D403C E0C200D8 E0C300F8 E1520004 BAFFFFFB E49D0004 E129F000 E8BD001F E28DD024 E12FFF1E 00000000 00000000 00000000 // r0, r1 00000000 00000000 // r2, r3 00000000 00000000 // r4, r5 00000000 00000000 // r6, r7 00000000 00000000 // r8, r9 00000000 00000000 // r10, r11 00000000 00000000 // r12, sp D2000000 00000000
CTRPFのコードに直すと上記の通り。これでr0~r13(sp)のレジスタ値が取得できる。
Armでのnopの扱い
Armにはnop
という「何もしない命令」が存在する。これは主に以下のような用途で使用される。
- 遅延処理
- 別の命令のダミー
- 命令のパディング
上記はあくまで一例であり、他にも用途は様々。 こんなnopだが、すべてのバージョンで同じようにnopを扱えるわけではない。
疑似命令として
従来のアセンブラではnopは疑似命令として扱われており、専用の命令が存在していなかった。ArmモードとThumbモードで扱いが異なり、以下のように変換される。
モード | 16進表現 | 意味 |
---|---|---|
Arm | 0xE1A00000 | mov r0, r0 |
Thumb | 0x46C0 | mov r8, r8 |
どちらも「同じレジスタに値をコピーする」という処理になるため、当然何も更新されない。
専用命令として
ArmにはARMv6Kから、ThumbにはARMv6T2から、「nop命令」というnop専用の命令が登場する。命令は以下の通り。
モード | 16進表現 |
---|---|
Arm | 0xE320F000 |
16-bit Thumb | 0xBF00 |
32-bit Thumb | 0x8000F3AF |
※対応バージョン以外では、これらの16進表現はnopの意味を成さないので注意。
CTRPFで学ぶソートアルゴリズム
はじめに
CTRPFと呼ばれるNintendo 3DS向けのプラグインシステムがある。今回はこれを用いて、ソートアルゴリズムについて学ぶ。
ソートアルゴリズム
データの集合を一定の規則に従って並べることを「ソート」と言う。このソートを行う手順や計算手法のことを「ソートアルゴリズム」という。今回はCTRPFを用いて、ソートアルゴリズムの一種である「バブルソート」を実装する。
バブルソートとは
バブルソートとは、隣り合う要素の大小を比較しながら整列するアルゴリズムのこと。例えば4, 3, 2
の順に並んでいる要素を昇順ソートすると、以下のような処理になる。
1周目
最初の2つを比較する。3より4の方が大きいため、3と4を交換する。
|4, 3|, 2 ↓ |3, 4|, 2
1つずらして比較。4と2では4の方が大きいため、2と4を交換する。
3, |4, 2| ↓ 3, |2, 4|
これで最大値を一番右に持ってくることができた。1周目終わり。
2周目
2周目では一番右に最大値があることが既に分かっているため、比較回数は1回でよい。3と2を比較して、3の方が大きいため交換。
|3, 2|, 4 ↓ |2, 3|, 4
これで昇順に並べ替えることができた。
アルゴリズム
上記の例では分かりにくかったかもしれないが、このアルゴリズムについて、要素数をnとすると以下のようなことが言える。
- 比較を繰り返す周の数はn-1周
- m周目に行う比較回数はn-m回
実装
さて、ここからが本題。上記のアルゴリズムを以下の条件を元に実装する。
- 要素のサイズは8bit
- r0は残りの周数
- r1は残りの比較回数
- r2は1番目の比較対象
- r3は2番目の比較対象
- r10は要素のポインタ(CTRPFの場合共有メモリのポインタ)
- 要素数は2以上256以下
- r10の操作にはスタックを使用
E92D4400 @ push {r10, lr} E3A000XX @ mov r0, #0xXX E2400001 @ sub r0, r0, #1 E1A01000 @ mov r1, r0 E5DA2000 @ ldrb r2, [r10] E5DA3001 @ ldrb r3, [r10, #1] E1520003 @ cmp r2, r3 C5CA3000 @ strbgt r3, [r10] C5CA2001 @ strbgt r2, [r10, #1] E2511001 @ subs r1, r1, #1 C28AA001 @ addgt r10, r10, #1 CAFFFFF7 @ bgt #-0x1c E2500001 @ subs r0, r0, #1 C59DA000 @ ldrgt r10, [sp] CAFFFFF3 @ bgt #-0x2c E8BD8400 @ pop {r10, pc}
以下各行解説。(見たいコードをクリック)
mov r1, r0
今回の実装方法の場合、残りの周数=その周での比較回数になる。各周のはじめにr1をr0で初期化しておく必要があるため、r0の値をr1にコピー。
ldrb r2, [r10]
1番目の要素をr2にロード。
ldrb r3, [r10, #1]
2番目の要素をr3にロード。
cmp r2, r3
r2とr3を比較。
strbgt r3, [r10]
1番目の要素(r2)の方が大きい場合は、2番目の要素(r3)を1番目の要素の位置に格納。
strbgt r2, [r10, #1]
1番目の要素(r2)の方が大きい場合は、1番目の要素(r2)を2番目の要素の位置に格納。
subs r1, r1, #1
r1をデクリメント。結果をステータスフラグに反映。
addgt r10, r10, #1
r1が0より大きい場合、r10をインクリメント。(次の要素を比較するため。)
bgt #-0x1c
r1が0より大きい場合、ldrb r2, [r10]
へと分岐。次の要素の比較が始まる。
subs r0, r0, #1
r0をデクリメント。結果をステータスフラグに反映。
ldrgt r10, [sp]
r0が0より大きい場合、spからr10をロード。このときsp(スタックポインタ)は最初にpushしたr10を指しているため、元のr10の値を復元するということになる。
bgt #-0x2c
r0が0より大きい場合、mov r1, r0
へと分岐。次の周の処理が始まる。
pop {r10, pc}
r10の値を復元、lrの値をpcにロードしサブルーチン終了。
CTRPFで実行
コードは先程の通り。今回は例として0x10バイトを昇順ソートする。
次に、共有メモリに0x10バイトのランダムな値を書き込む。
そして先程のソートコードを実行すると、、、?
正しく昇順ソートとなっていることが分かる。
まとめ
アルゴリズムの知識は必要不可欠。いくらプログラミングの知識を持っていても、効率の悪いコードを書いていては意味がない。他にもアセンブリで簡単に実装できるアルゴリズムは色々とあるので、是非試してみよう。
命令の末尾に"s"をつけると・・・?(Arm)
はじめに
皆さんはArm命令の末尾にs
がついた命令を見たことがあるだろうか。見たことはあっても、通常の命令と何が違うのかイマイチ分からない、という方も多いかもしれない。今回はこのs
がどのような意味を持つのか詳しく見てみよう。
sの意味
末尾にs
を付けると、演算結果をステータスフラグに反映してくれるようになる。具体的には、第一オペランドのレジスタに入る値(演算結果)を元にステータスフラグが更新される。
ステータスフラグ
ステータスフラグについておさらい。皆さんはcmp命令やtst命令について知っているだろうか。条件実行する際によく使用される命令である。これらは正確には「演算結果をステータスフラグに反映する命令」である。
ステータスフラグには上記のようなものがある。cmp命令は第一オペランドから第二オペランドを引いた値が、tst命令は第一オペランドと第二オペランドの論理積が演算結果として反映される。
また、条件実行の概要については上記の通り。条件付き命令は、その時点でのステータスフラグの状態と条件が一致した場合に実行される。
例えば以下のようなコードがあったとする。
mov r0, r1 cmp r0, r1 beq label
当然このコードはlabel
へと分岐する。普通なら「r0とr1が同値だから・・・」でいいのだが、今回はもう少し掘り下げて考えてみる。
先程述べた通り、cmp命令はオペランドの差分をステータスフラグに反映する。つまり今回の場合はr0-r1、すなわち0である。演算結果が0ということは、Zフラグが1に設定される。eq
はZが1に設定されていることが実行条件であるため、beq label
は実行されるというわけである。
sを使ってみる
本題に戻る。実はこのステータスフラグの概念を理解していると、cmp命令やtst命令を使わずに条件実行できる場合がある。例えば以下のようなコード。
mov r0, r1 cmp r0, #0 beq label
r1をr0に代入して、その値が0なのであればlabelに分岐するという処理である。一見なんの無駄も無いように思えるが、実はこのような命令は短縮できる。
注目すべきはcmp r0, #0
。これは「r0 - 0
の演算結果をステータスフラグに反映しろ」という意味である。つまり、実際ステータスフラグに反映するのはr0の値。直前にmov r0, r1
があるため、この時点でr0の値はステータスフラグに反映できる。よって、以下のようなコードに書き換えることができる。
movs r0, r1 beq label
これで1命令減らすことができた。
cmp命令やtst命令との違い
では、cmp命令やtst命令とはどのように使い分ければよいだろうか。結論から言うと、s
を用いることが可能であれば使用し、そうでなければcmp命令やtst命令を使えばよい。例えばldr命令にはs
をつけることができないためcmp命令やtst命令を使用し、演算命令(movやaddなど)ではs
を使うといった具合だ。
まとめ
末尾s
の役割は分かっていただけただろうか。ステータスフラグの概念を理解しこれらをうまく使い分ければ、よりコンパクトなコードが書けるようになる。積極的に活用してみよう。
即値もレジスタも自由に操作!バレルシフタの役割(Arm)
はじめに
ArmをはじめとするRISCプロセッサは固定長命令であるため、32bitの即値をそのまま使用することはできない。Armの命令では、下位8bitが即値に割り当てられることが多い。
「それだと8bitサイズの即値しか使えないじゃないか!!」そう思った方がいるかもしれない。そこで登場するのがバレルシフタである。
バレルシフタとは
バレルシフタとは、データをシフト処理するデジタル回路のことである。レジスタのデータはALU(演算装置)に渡され、そこではじめて演算が行われる。バレルシフタは、命令に含まれるシフト情報を用いて、データがALUに渡される前にデータにシフト処理を行う。この機能により、8bitの即値を事前にシフトして大きくしたり、レジスタの値をシフト処理した状態で演算に使用したりできる。
即値の場合
即値の場合は、8bitをシフト処理して表現可能な値であれば指定可能。実際には「8bit値を偶数回右ローテートして表現可能な値かどうか」をアセンブラが判断する。例えばmov r0, #0x80000000
は1命令で実行できる。これは正確にはmov r0, #2, #2
で、「2を2回右ローテートした値(=0x80000000)をr0に代入」という意味である。
これがどういう意味かは機械語表現すると分かりやすい。mov r0, #0x80000000
は機械語表現すると0xE3A00102だが、この構造は以下の通りである。
まずMOV構造について簡単に説明。Iフラグは即値かどうか、Sフラグはcpsrに反映するかどうかを意味する。今回は即値指定でcpsrには反映しないため、I=1, S=0である。即値が指定されている場合は、シフト値×2回(偶数回)即値を右ローテートする。
次に16進数の欄を見てみる。レジスタは0(r0)、シフト値は1、即値は2である。これを先程の規則に従って解釈すると、mov r0, #2, #2
、すなわちmov r0, #0x80000000
となる。
レジスタの場合
レジスタの場合は論理左シフト(LSL)、論理右シフト(LSR)、算術右シフト(ASR)、右ローテート(ROR)が使用可能。例として、r1を2で割った値とr0を比較するコード(cmp r0, r1, lsr #1
)をエンコードしてみる。これは機械語では0xE15000A1である。
ID | 種類 | 意味 |
---|---|---|
00 | LSL | 論理左シフト |
01 | LSR | 論理右シフト |
10 | ASR | 算術右シフト |
11 | ROR | 右ローテート |
今回は即値ではないためIフラグには0が設定される。Rnは第一オペランド、Rmは第二オペランド。種類の欄にはシフトのIDが対応する。Rn=0(r0)、シフト回数=1、ID=1(lsr)、Rm=1(r1)。よってcmp r0, r1, lsr #1
となる。
まとめ
バレルシフタを活用すれば、使用するレジスタを減らせたり、命令数を減らせたりと数々の恩恵を受けることができる。Armに触れる際はぜひ意識してみよう。
x86-64 呼び出し規約(概略)
Microsoft x64
引数
型 | 第1 | 第2 | 第3 | 第4 |
---|---|---|---|---|
整数・ポインタ | RCX | RDX | R8 | R9 |
浮動小数点 | XMM0 | XMM1 | XMM2 | XMM3 |
- レジスタだけでは引数が不足する場合、スタックを使用する。
戻り値
整数型・ポインタ型はRAXを、浮動小数点型はXMM0を使用して返される。
System V AMD64 ABI
引数
型 | 第1 | 第2 | 第3 | 第4 |
---|---|---|---|---|
整数・ポインタ | RDI | RSI | RDX | RCX |
浮動小数点 | XMM0 | XMM1 | XMM2 | XMM3 |
型 | 第5 | 第6 | 第7 | 第8 |
---|---|---|---|---|
整数・ポインタ | R8 | R9 | ||
浮動小数点 | XMM4 | XMM5 | XMM6 | XMM7 |
戻り値
RAXを使用して返される。
アライメント
x86-64では16バイトにアラインされる。