bamboo’s blog

Bambooの気まぐれブログ

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)

概要

 きっかけはどぅーむーんさんのツイート。

内容としては、hookコードでレジスタ確認を行うというもの。実装は少々面倒かもしれないが、面白そうだったので組んでみることにした。(十分なテストを行っていないため、間違い等あればコメント欄まで)

手順

 以下の手順で実装する。今回の方法ではlrとpcを参照できないため、確認するレジスタはr0~r13(sp)とする。

  1. r0~r13, cpsrをスタックに退避。
  2. スタックから値を読み込み、実行コードの下部に書き込む。
  3. 上書きしたレジスタを復元し、スタックポインタを戻す。

※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の位置を読み込む。

ldrd r0, [r2], #8  r2から値をロード。ldrdはダブルワードをロードする命令なので、r0には偶数レジスタが、r1には奇数レジスタが読み込まれる。ポインタを次の読み込み元にずらすため、ロード後r2には8が加算される。

strd r0, [r3], #8  r3のメモリアドレスにレジスタ2つ分書き込む。次のレジスタを書き込むため、ストア後r3には8が加算される。

cmp r2, r4  r2とr4を比較。r2は次のロード開始位置、r4は元のスタックポインタの位置を指している。つまり、「ロードすべきレジスタが残っているか」を判断している。

blt #-12  r2の方が小さければまだロードしていないレジスタがあるということなので、次のレジスタのロードを開始する。

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}

 以下各行解説。(見たいコードをクリック)

push {r10, lr}  r10とlrをスタックに積む。ここで注意すべき点は、複数のレジスタを同時にスタックに積む場合、レジスタ番号の小さい方が後(低位アドレス)に積まれることである。例えば、push {r4-r6}の直後にldr r0, [sp]とすると、r4の値がr0にロードされる。

mov r0, #0xXX  要素数をr0に読み込む。XXには要素数を指定。

sub r0, r0, #1  バブルソートの説明の箇所で述べた通り、比較を繰り返す周の数は素数-1である。この値をr0にセットする。

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バイトを昇順ソートする。

f:id:bamboo_cpu:20210303211501j:plain
Bubble sort in CTRPF

次に、共有メモリに0x10バイトのランダムな値を書き込む。

f:id:bamboo_cpu:20210303212240j:plain

そして先程のソートコードを実行すると、、、?

f:id:bamboo_cpu:20210303212403j:plain

正しく昇順ソートとなっていることが分かる。

まとめ

 アルゴリズムの知識は必要不可欠。いくらプログラミングの知識を持っていても、効率の悪いコードを書いていては意味がない。他にもアセンブリで簡単に実装できるアルゴリズムは色々とあるので、是非試してみよう。

命令の末尾に"s"をつけると・・・?(Arm)

はじめに

 皆さんはArm命令の末尾にsがついた命令を見たことがあるだろうか。見たことはあっても、通常の命令と何が違うのかイマイチ分からない、という方も多いかもしれない。今回はこのsがどのような意味を持つのか詳しく見てみよう。

sの意味

 末尾にsを付けると、演算結果をステータスフラグに反映してくれるようになる。具体的には、第一オペランドレジスタに入る値(演算結果)を元にステータスフラグが更新される。

ステータスフラグ

 ステータスフラグについておさらい。皆さんはcmp命令やtst命令について知っているだろうか。条件実行する際によく使用される命令である。これらは正確には「演算結果をステータスフラグに反映する命令」である。

f:id:bamboo_cpu:20210225132945p:plain

 ステータスフラグには上記のようなものがある。cmp命令は第一オペランドから第二オペランドを引いた値が、tst命令は第一オペランドと第二オペランド論理積が演算結果として反映される。

f:id:bamboo_cpu:20210225133235p:plain

 また、条件実行の概要については上記の通り。条件付き命令は、その時点でのステータスフラグの状態と条件が一致した場合に実行される。
 例えば以下のようなコードがあったとする。

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だが、この構造は以下の通りである。

f:id:bamboo_cpu:20210224005855p:plain

 まず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である。

f:id:bamboo_cpu:20210224025816p:plain

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バイトにアラインされる。