リソース#
アセンブリ言語には変数の概念はなく、アセンブリ言語は通常レジスタを操作します。算術命令のオペランドはレジスタから取得する必要があり、ハードウェアに組み込まれた特別な位置(CPU 内?)から取得されます。
{{< block type="tip" >}}
レジスタ(Register)は中央処理装置内で命令、データ、およびアドレスを一時的に保存するためのコンピュータストレージです。レジスタのストレージ容量は限られており、読み書き速度は非常に速いです。コンピュータアーキテクチャでは、レジスタは既知の時間点で行われた計算の中間結果を保存し、データに迅速にアクセスすることでコンピュータプログラムの実行を加速します。
{{< /block >}}
RISC-V カード#
RISC-V オペランド#
- レジスタのサイズが 64 ビットの場合はダブルワード、32 ビットの場合はシングルワードと呼ばれます。
- x0 はハード接続されて 0 に設定されています。
add x3, x4, x0
=>x3 = x4
(x0 は値 0 にハード接続されています)
アセンブリ命令#
ストレージオペランド#
{{< block type="tip" >}}
メモリからレジスタにデータをコピーするデータ転送命令は ロード命令 (load
) と呼ばれます。RISC-V では命令は ld で、ダブルワードを取得します。
{{< /block >}}
配列から値を取得する C プログラムのアセンブリコード#
g = h + A[8];
A は 100 個のダブルワードで構成される配列で、g と h はそれぞれ x20 と x21 に格納され、配列の開始アドレスまたはベースアドレスは x22 に格納されています。
ld x9, 8(x22) // x9 = A[8]
add x21, x20, x9; // x21 = x20 + x9
ベースアドレスを格納するレジスタ(x22)はベースアドレスレジスタと呼ばれ、データ転送命令の 8 はオフセットと呼ばれます。
{{<block type="tip" title="ビッグエンディアンとリトルエンディアンのアドレッシング">}}
コンピュータは二種類に分かれ、一つは最左端または「ビッグエンディアン」バイトのアドレスをダブルワードアドレスとして使用し、もう一つは最右端または「リトルエンディアン」バイトのアドレスをダブルワードアドレスとして使用します。
RISC-V はリトルエンディアンを使用します。ダブルワード形式で 8 個の個別のバイトに同じデータにアクセスする場合にのみ、バイト順序が影響を与えるため、ほとんどの場合「エンディアン」を気にする必要はありません。
{{< /block >}}
したがって、上記のコードが正しいバイトアドレスを取得するために、x22 というレジスタのオフセットは 64(8x8)になります。
ロード命令とは逆の命令は通常 * ストア命令 (store)* と呼ばれ、レジスタからメモリにデータをコピーします。命令は sd
で、ダブルワードをストアします。
{{< block type="tip">}}
いくつかのアーキテクチャでは、ワードの開始アドレスは 4 の倍数でなければならず、ダブルワードの開始アドレスは 8 の倍数でなければなりません。この要件はアライメント制限と呼ばれます。
{{< /block >}}
RISC-V と Intel x86 にはアライメント制限はありませんが、MIPS にはこの制限があります。
ロードとストアを使用して生成された命令#
A[12] = h + A[8];
h は x21 に格納され、A のベースアドレスは x22 に格納されています。
ld x9, 64(x22) // x9 = A[8]
add x9, x21, x9 // x9 = h + A[8]
sd x9, 96(x22) // A[12] = x9
文字列コピープログラムをアセンブリにコンパイル#
void strcpy(char x[],char y[]){
size_t i;
i = 0;
while((x[i] = y[i]) != '\0'){
i += 1;
}
}
x, y のベースアドレスは x10 と x11 に格納され、i は x19 に格納されています。
strcpy:
addi sp, sp, -8 // スタックポインタを調整し、1つのアイテム(x19)を格納
sd x19, 0(sp) // x19 をスタックにプッシュ
add x19, x0, x0 // x19 = 0 + 0
L1: add x5, x19, x11 // x5 = x19 + x11 => y[i] のアドレスを x5 に格納
lbu x6, 0(x5) // temp: x6 = y[i]
add x7, x19, x10 // x5 = x19 + x11 => x[i] のアドレスを x7 に格納
sd x6, 0(x7) // x[i] = y[i]
beq x6, x0, L2 // if x6 ==0 then go to L2
addi x19, x19, 1 // i = i + 1
jal x0, L1 // go to L1
L2: ld x19, 0(sp) // x19 とスタックポインタを復元
addi sp, sp, 8
jalr x0, 0(x1)
ループコードをアセンブリにコンパイル#
int A[20];
int sum = 0;
for (int i = 0; i < 20; i++){
sum += A[i];
}
RISC-V アセンブリ(32 ビット)
add x9, x8, x0 # x9 = &A[0]
add x10, x0, x0 # sum
add x11, x0, x0 # i
addi x13,x0, 20 # 20
Loop:
bge x11, x13, Done # if x11 > x13 go to Done (end loop)
lw x12, 0(x9) # x12 = A[i]
add x10, x10, x12 # sum
addi x9, x9, 4 # x9 = &A[i+1]
addi x11, x11, 1 # i++
j Loop
Done:
論理操作#
and
andi
and x5, x6, x9
=> x5 = x6 & x9addi x5, x6, 3
=> x5 = x6 & 3
sll
slli
, 左シフト(拡大)slli x11, x23, 2
=> x11 = x23 << 2- 0000 0010 => 2
- 0000 1000 => 8
srl
srli
, 右シフト(縮小)srli x23, x11, 2
= > x23 = x11 >> 2- 0000 1000 => 8
- 0000 0010 => 2
sra
srai
, 算術右シフト- 1111 1111 1111 1111 1111 1111 1110 0111 = -25
srai x10, x10, 4
- 1111 1111 1111 1111 1111 1111 1111 1110 = -2
RISC-V アセンブラの便利な機能#
- a0 - a7 はパラメータレジスタ(x10 - x17、関数呼び出し用)。
- zero は x0 を表します。
mv rd, rs = addi rd, rs, 0
li rd, 13 = addi rd, x0, 13
nop = addi x0, x0
la a1 Label
は Label のアドレスを a1 にロードします。- a0 - a7(x10 - x17):8 つのレジスタはパラメータの受け渡しと 2 つの戻り値(a0 - a1)に使用されます。
- ra(x1):戻りアドレスのレジスタで、呼び出し元(呼び出し位置)に戻るために使用されます。
- s0 - s1(x8 - x9)および s2 - s11(s18 - x27):保存されたレジスタです。
RISC-V 関数呼び出しの変換#
- レジスタはメモリよりも速いため、レジスタを使用します。
jal rd, Label
ジャンプとリンクjal x1, 100
jalr rd, rs, imm
ジャンプとリンクレジスタjalr x1, 100(x5)
jal Label
=>jal ra, Label
関数を呼び出します。jalr s1
がメソッドポインタである場合、これは関数呼び出しです。
関数呼び出しをアセンブリに変換#
...
sum(a,b);
...
int sum(int x, int y){
return x + y;
}
1000 mv a0, s0 # x = a
1004 mv a1, s1 # y= b
1008 addi ra, zero, 1016 # 1016 は sum 関数
1012 j # sum にジャンプ
1016 ...
...
2000 sum: add a0, a0, a1
2004 jr ra
1008 ~ 1012 は jal sum
に置き換えることができます。
関数呼び出しの基本ステップ#
- 必要なパラメータをメソッドがアクセスできる場所(レジスタ)に置きます。
- 制御を関数に移譲し、(
jal
) を使用します。- アドレスを保持し、関数のアドレスにジャンプします。
- 関数の実行に必要な(ローカル)ストレージリソースを取得します。
- 期待される関数を実行します。
- 戻り値を呼び出しコードがアクセスできる場所に置き、使用したレジスタを復元し、ローカルストレージを解放します。
- コントローラーをメインプロセッサに戻します(
ret
)、レジスタに保存されたアドレスを使用して、呼び出し元に戻ります。
メソッド呼び出しの例#
int leaf(int g, int h, int i, int j){
int f;
f = (g + h) - (i + j);
return f;
}
- g,h,i,j は a0,a1,a2,a3 に格納されます。
- f は s0 に格納されます。
- temp は s1 です。
leaf:
# プロローグ開始
addi sp, sp, -8 # 2 つの整数を保存するために 8 バイトを確保
sw s1, 4(sp) # s1, s0 を sp に保存
sw s0, 0(sp)
# プロローグ終了
add s0, a0, a1 # f = g + h
add s1, a2, a3 # temp = i + j
sub a0, s0, s1 # a0 = (g + h) - (i + j)
# エピローグ
lw s0, 0(sp) # s1, s0 を復元
lw s1, 4(sp)
addi sp, sp, 8
jr ra
sp#
{{< block type="tip" >}}
sp はスタックポインタで、メモリ空間の最上部から下に向かって増加します。RISC-V では x2 というレジスタを使用します。
- push は sp のポインタアドレスを減少させます。
- pop は増加させます。
{{< /block >}}
各関数にはスタック上にデータのセットがあり、これをスタックフレーム(stack frame)と呼びます。スタックフレームには通常、次のものが含まれます:
- 戻りアドレス
- パラメータ
- 使用されるローカル変数のスペース
ネストされた関数呼び出し#
int sumSquare(int x,int y){
return mult(x,x) + y;
}
ra には sumSquare に戻る値があり、この値は mult によって上書きされます。
- caller: 関数を呼び出す人
- calle: 呼び出される関数
- 呼び出された側が実行から戻るとき、呼び出し側はどのレジスタが変更される可能性があるか、どのレジスタが変更されないことが保証されているかを知る必要があります。
- レジスタの規則: どのレジスタが関数呼び出し(
jal
)後にキャッシュが無効になり、どのレジスタが変更可能か。- 一部のレジスタは揮発性(temp)であり、一部は保存されるべきです(呼び出し側は元の値を復元する必要があります)。
- これにより、スタックフレームに入るたびにレジスタの数が最適化されます。
- 分類:
- 関数呼び出し間で保持されるもの:
- sp, gp, tp
- s0 - s11 (s0 も fp)
- 保持されないもの:
- パラメータレジスタおよび戻りレジスタ: a0 - a7, ra
- temp レジスタ: t0 - t6
- 関数呼び出し間で保持されるもの:
上記のコードの RISC-V
x は a1 に、y は a1 に格納されます。
sumSquare:
addi sp, sp, -8
sw ra, 4(sp) // 戻りアドレスを sp に保存
sw a1, 0(sp) // s1 を y に保存
mv a1, a0 // y = x => mult(x,x)
jal mult // mult を呼び出す
lw a1, 0(sp) // sp から y を取得
add a0, a0, a1 // mult() + y
lw ra, 4(sp) // sp から戻りアドレスを取得
addi sp, sp, 8
jr ra
RISC-V レジスタ名#
RISC-V メソッド呼び出しのパターン#
matmul:
# スタックにプッシュし、使用するいくつかの s レジスタを保存するためのスペースを確保
addi sp, sp, -36
sw ra, 0(sp)
sw s0, 4(sp)
sw s1, 8(sp)
sw s2, 12(sp)
sw s3, 16(sp)
sw s4, 20(sp)
sw s5, 24(sp)
sw s6, 28(sp)
sw s7, 32(sp)
body:
# xxx xxx
end:
# レジスタの値を復元
lw ra, 0(sp)
lw s0, 4(sp)
lw s1, 8(sp)
lw s2, 12(sp)
lw s3, 16(sp)
lw s4, 20(sp)
lw s5, 24(sp)
lw s6, 28(sp)
lw s7, 32(sp)
addi sp, sp, 36
ret
RISC-V 命令のバイナリ表現#
R フォーマットレイアウト#
算術および論理演算命令に使用されます。
- opcode, funct3, funct7 : 加算、減算、左シフト、排他的論理和などの操作を実行するかどうかを示します。
- R-format の opcode は固定で 0110011 です。
- add 操作:
add x18 x19 x10
=>x18 = x19 + x10
0000000 01010 10011 000 10010 0110011
rs2 = x19
,rs1 = x10
,rd = x18
I フォーマットレイアウト#
即値を処理します。例えば addi rd rs1, imm
=> addi a0 a0 1
- imm の範囲は -2084 ~ 2047 です。
RISC-V ロード#
ロード命令も I タイプです。