Raspberry Pi 3でアセンブラをはじめから〜基礎編〜


はじめに

Web系エンジニアも低レイヤーを学ぶべきと巷で言われていますが、本当なのだろうか。。。

基本情報技術者試験を勉強するという選択肢もありましたが、個人的に分かりにくかったので(レジスタはCPU内部にある記憶装置って言われてもピンとこない)、アウトプット主体で学んで、その都度理解できない概念を調べてインプットしていくことにしました。

アウトプットの方法として、前から触ってみたかったRaspberry Pi 3(ラズパイ)を購入してアセンブラを約1ヶ月ほど勉強してみました。同じような方々の助けになれば幸いです。

今記事ではラズパイを使用するためARMでの実装です。
「ARMとは?」と言ったアーキテクチャから発展編では簡単なアルゴリズムを実装するまで解説しています。

間違っているところがあればご指摘いただけるとありがたいです。

現在の記事 → Raspberry Pi 3でアセンブラをはじめから〜基礎編〜
作成中 → Raspberry Pi 3でアセンブラをはじめから〜発展編〜

セットアップ

具体的なセットアップ方法はこちら

私が用意したものは以下の7点です。
ラズパイ(Raspberry Pi 3を使用)
microSDカード(スターターキットで既に付いている場合は不要)
アダプター(スターターキットで既にmicroSDカードにOSがインストールされている場合は不要)
・モニター
・HDMLケーブル
・有線マウス
・有線キーボード

*時々死んでしまうため、OSの再インストールが必要な場合はこちら

前提知識

そもそもアセンブラとは

C言語のコンパイルgcc main.cをするだけで可能ですが、実際は以下の4つの手順を踏んでいます。
1. main.cをpreprocessorでコンパイラの前処理をする。簡単にいうと別のC言語「.i」に変換している(preprocessorとは#include <stdio.h>のようなディレクティブのこと)
$gcc -E main.c「.i」ファイルができるが基本的に2をすれば自動的に1もされる。
2. 「.i」をアセンブラ「.s」に直す。コンピュータが解釈できるアセンブリ言語の形式に変換。
$gcc -S main.c
3. 「.s」を機械語「.o」に直す。
$gcc -o {実行ファイル名} main.s
4. 実行ファイルに直す。
$./{実行ファイル名}

アセンブラは「.s」拡張子なので、1, 2を飛ばして3から始めます。
機械語に近い低水準言語を学ぶことで、コンピューターの生の動作を意識した無駄のないプログラムを高水準言語でも作れるようになります。

Instruction set architecture(ISA)

CPUのアーキテクチャの種類としてx86とARMが2大巨塔です。
有名どこだと、2020年以降MacはIntel製のx86アーキテクチャから自社開発のARMアーキテクチャに切り替わりました。

x-86: 高性能+消費電力大
ARM: 性能にばらつきあり+省電力
という違いがあるそうです。だからM1 Macからバッテリー駆動時間が爆発的に伸びたのかな?

ちなみに、x-86なのかARMなのかで、Dockerイメージを使い分けないといけません。M1 Macに変えてこれまでのDockerfileが使えないなんてよく騒がれてましたが、CPUのアーキテクチャが原因なので対応すれば普通に使えます。

レジスタ

r0からr15までの16個のレジスタがあり、limited memoryのようなイメージ。一時記憶領域のため、プログラムが終了すると消えてしまうが最も高速。


r0~r10: 一般的な目的で使われる

r0~r3に第1~第4引数までセットされ、r0に戻り値が入る。それ以降はスタック

r4以降は普通のレジスタ

r11~r15: 特別な目的で使われる

r11(fp): frame pointer

r12(ip): intra-pocedure call scratch pointer(よく分からない)

r13(sp): stack pointer

r14(lr): link register(BL命令で分岐した場合の戻りアドレスを保持)

r15(pc): program counter(実行中のメモリアドレスを保持)

ディレクティブの定義

.text
テキストセグメントの開始を指定する.ARM命令などの実行コードはテキストセグメントに配置する必要がある。
.align 2
Raspberry Pi 3は32ビットOSがインストールされているため、4byte単位でメモリが作られる。
.global func
外部ファイルからもアクセス可能にする。

ARM命令セット

命令フォーマット 説明
mov r1, #4 r1 = 4 *説明あり
add r1, r2, r3 r1 = r2 + r3
sub r1, r2, r3 r1 = r2 - r3
mul r1, r2, r3 r1 = r2 * r3
udiv r1, r2, r3 r1 = r2 / r3
b branch branch(自分で命名)にジャンプ
bl func func(自分で命名もしくは既存のものを使用)関数の呼び出し
bx lr レジスタlrの指す命令へジャンプ
ldr r0, address レジスタ←memory(load command)
str r0, address レジスタの値をaddressにストア(store command)
ldr r0, address addressをレジスタに移動(load command)
cmp r0, r1 r0とr1を比較 *説明あり

movの説明

数字は#を使用しなくてもいいが、古いバージョンでは必要なので数字の前に書いておく。
アセンブリ言語は高水準言語と違って、1行で四則演算をできないため分けて書く必要がある。
また、udivを使う際は下記の実践編で仕様している.cpu cortex-a53, .fpu neon-fp-armv8を記述する必要がある、

cmpの説明

cmpの後に比較結果を判別する必要がある。

mov r0, #10
cmp r0, #4 @r0と4を比較する
bgt b big @r0>4であれば自分で命名したinner関数にジャンプ

big: 
 @inner関数のなかみ
比較方法 イメージ 説明
bne branch r0 != 4の時branchにジャンプ not equal
beq branch r0 == 4の時branchにジャンプ equal
blt branch r0 < 4の時branchにジャンプ less than
bgt branch r0 > 4の時branchにジャンプ less than
ble branch r0 <= 4の時branchにジャンプ less or equal
bge branch r0 >= 4の時branchにジャンプ grater or equal

この条件に合わない場合は、その行をスキップして次の行に進う。
またbXX branchb + 比較なので、ジャンプするbranchを記述している。

実践

Hello Worldを実行してみる

main.s
@この2行はおまじないとして書いている
.cpu cortex-a53 @浮動小数点アーキテクチャ
.fpu neon-fp-armv8 @コンパイラオプション

.data
prompt: .asciz "Hello World!\n" @メモリ

.text
.align 2
.global main @main関数をglobalにして外からでも呼べるように
.type main, %function

main:

mov r4, lr @r4=lr
ldr r0, =prompt @メモリからレジスタに移動 r0="Hello World!\n"
bl printf @branch labeled: printするfunctionを呼ぶ

mov r0, #0 @r0=0
mov lr, r4 @lr=r4
bx lr @branch x link register: returnする

bl printfで表示されるのはr0の内容なので、Hello World!が表示。

インプット

main.s
.cpu cortex-a53
.fpu neon-fp-armv8

.data
prompt: .asciz "Enter the number: "
inp: .asciz "%d"
outp: .asciz "The number that you input is %d\n"

.text
.align 2
.global main
.type main, %function

main:

mov r7, lr
ldr r0, =prompt
bl printf

@ scanf("%d", &sp);
sub sp, sp, #4 @ アドレスが4byte単位なので、inputのデータ保持する空箱をストアする。
ldr r0, =inp @ 第一引数"%d"をr0に
mov r1, sp @ 第二引数にアドレスspを入れる。
bl scanf @ scanfメソッド発火。
ldr r4, [sp] @ spはアドレスなのでdereferenceする。

@ printf("The number that you input is %d\n", r1);
ldr r0, =outp
mov r1, r4
bl printf

@ return 0;
mov r0, #0
mov lr, r7
bx lr

発展編へ

発展編ではarrayなどをARMアセンブラを用いて解説しています。

参考文献

プログラムはなぜ動くのか
ARMについて
You Can Learn ARM Assembly Language in 15 Minutes | ARM Hello World Tutorial