x86 で asm 書く機会ってほんとなくて、むしろ ARM の命令セットを学んだほうがいろいろモチベーションがでてきそう、ということで、とりあえず ARM Linux 上で簡単なのを書いてみます。

ARM

ARM は伝統的に OABI ("arm"), EABI ("armeb")という2つのインターフェイスがあるらしいです。OABI が古いほうで汎用 (Old)。EABI は組込み (Embeded) 用。ABI の違いということはシステムコールの呼びかたとか、サブルーチンの呼びかたが違いということです…… すなわちアセンブリで書くときはどっちを書いてるか明確にする必要があります。

ここでは EABI だけでやってみます。EABI の場合システムコールは、r7 にシステムコール番号を指定して、SVC 命令を発行 (オペランドは 0) すれば良いようです。

環境は GCC でやります。

その前に

アセンブリ書くとき、そもそも命令セット (instruction set) がわからないとどうにもならない。リストはARM のサイトから探す。

x86 とかも、とにかく命令セットがわからないと何もしようがない。x86 instruction set とかでググったらよさそう。x86 は命令いっぱいありすぎて pdf が4つぐらいにわかれてる。

ただ、GCC で書く場合 gas (gnu as) のフォーマットで書かないといけないので、データシートと形式がちがうのが罠い。

GCC as (gas) での書きかたは、公式のドキュメントを参照する必要がある。x86 と arm でも書きかたが違う。

exit するだけのコード

まず exit するだけのコードを書いてみます。

/*#!as -o sketch.o sketch.s && ld -o sketch -e _start sketch.o && ./sketch
 */

.global _start
_start:
        mov r0, $0x00 /* set exit status to 0 */
        mov r7, $0x01 /* set system call number to 1 (exit) */
        svc $0x00     /* supervisor call  */
$ as -o sketch.o sketch.s
$ ld -o sketch -e _start sketch.o
$ ./a.out

簡単!!!

r7 にシステムコール番号を入れ、r0 には exit の唯一の引数である終了ステータスを入れ、svc 命令でシステムコールが実行されます。当然 exit なので返ってきません。

obj-dump するとそのままのコードが出てきます

Disassembly of section .text:

00008054 <_start>:
    8054:       e3a00000        mov     r0, #0
    8058:       e3a07001        mov     r7, #1
    805c:       ef000000        svc     0x00000000

Hello, World

こっちは微妙に面倒です。普通に同じようにシステムコールを呼ぶだけですが、文字列をどこに置くか、どうやってアドレスを参照するかがいろいろあります。

.text に一緒に置く

たぶん一番シンプルな例

/* text section */

.section .text
.global _start

_start:
        mov r0, $0x01 /* set file descripter to 1 (stdout) */
        adr r1, msg   /* adr is GNU as's special instruction. This is translated to add */
        mov r2, $0x0d /* set msg length to 0x0d (13) */
        mov r7, $0x04 /* set system call number to 4 (write) */
        svc $0x00     /* supervisor call  */

        mov r0, $0x00 /* set exit status to 0 */
        mov r7, $0x01 /* set system call number to 1 (exit) */
        svc $0x00     /* supervisor call  */

msg:
        .asciz "Hello, World\n"
        .align 2

1行ごとにコメントつけてますが、この例では "Hello, World\n\0" という文字列が text セクションに置かれ、それを adr 命令で参照して r1 に書きこんでいます。adr 命令は GNU as が解釈して、指定したラベルを示すように add 命令に書きかえられます。試しにこれをアセンブルしたものを objdump すると

Disassembly of section .text:

00008054 <_start>:
    8054:       e3a00000        mov     r0, #0
    8058:       e28f1014        add     r1, pc, #20
    805c:       e3a0200d        mov     r2, #13
    8060:       e3a07004        mov     r7, #4
    8064:       ef000000        svc     0x00000000
    8068:       e3a00000        mov     r0, #0
    806c:       e3a07001        mov     r7, #1
    8070:       ef000000        svc     0x00000000

00008074 <msg>:
    8074:       6c6c6548        .word   0x6c6c6548
    8078:       57202c6f        .word   0x57202c6f
    807c:       646c726f        .word   0x646c726f
    8080:       000a            .short  0x000a
        ...

とかなってます。_start の中の adr があったところは add 命令に書きかわってます。.text はプログラム用のセクションですが、普通にデータも置けます。

.data セクションに置く

もう少しまともな例です。

/* text section */

.section .text
.global _start

_start:
        mov r0, $0x01   /* set file descripter to 1 (stdout) */
        ldr r1, msg     /* load memory address of msg to register  */
        ldr r2, msg_len /* load memory address of msg_len to register  */
        mov r7, $0x04   /* set system call number to 4 (write) */
        svc $0x00       /* supervisor call  */

        mov r0, $0x00 /* set exit status to 0 */
        mov r7, $0x01 /* set system call number to 1 (exit) */
        svc $0x00     /* supervisor call  */

msg:
        .word data_msg /* write data_msg address to here */
msg_len:
        .word data_msg_len

/* data section */
.section .data
data_msg:
        .asciz "Hello, World\n"
data_msg_len = . - data_msg
        .align 2

テキストデータ本体は data セクション (初期化される・書き換え可能・Cで書いた場合グローバル変数の領域) において、text セクションにはポインタのアドレスを置いています。ldr 命令でそのアドレスをレジスタの読みこみ、write を呼んでいます。

ついでに文字列の長さもアセンブラで計算させています。. は現在のアドレスを示していて、. から data_msg のアドレスを引くことで文字列の長さを求められます。

これを objdump -d -j .text -j .data sketch すると

Disassembly of section .text:

00008074 <_start>:
    8074:       e3a00000        mov     r0, #0
    8078:       e59f1014        ldr     r1, [pc, #20]   ; 8094 <msg>
    807c:       e59f2014        ldr     r2, [pc, #20]   ; 8098 <msg_len>
    8080:       e3a07004        mov     r7, #4
    8084:       ef000000        svc     0x00000000
    8088:       e3a00000        mov     r0, #0
    808c:       e3a07001        mov     r7, #1
    8090:       ef000000        svc     0x00000000

00008094 <msg>:
    8094:       0001009c        .word   0x0001009c

00008098 <msg_len>:
    8098:       0000000e        .word   0x0000000e

Disassembly of section .data:

0001009c <data_msg>:
   1009c:       6c6c6548        .word   0x6c6c6548
   100a0:       57202c6f        .word   0x57202c6f
   100a4:       646c726f        .word   0x646c726f
   100a8:       000a            .short  0x000a
        ...

みたいな感じになります。

ちなみに、ld に --verbose オプションをつけるとリンカスクリプトが表示されます。これはコードやデータを実際にメモリ上にどのように配置するかを定義したもので、ld はこの定義に従って、シンボルを解決 (アドレスを決めるということ) オブジェクトを配置します。

  1. トップ
  2. tech
  3. ARM アセンブリ Hello, World on Raspberry Pi

[tech] perl 5.19.9 の signatures 構文 (普通に引数を書ける構文) を試す | Mon, Feb 24. 2014 - 氾濫原 で「ただ引数の名前とかは外からとることができない。せっかく構文に組込まれたのなら、とれてもよさそうだなと思った」と書いたので、すこしだけ実装を書いてみた。

blead perl ecb4de39577c95734821743685366f6fe7f59a2d へのパッチです。

#!./perl -Ilib
use v5.19;
use strict;
use warnings;
use feature 'signatures';
no warnings "experimental::signatures";
use Test::More;
package signatures {
sub args ($sub) {
$signatures::subs{$sub+0};
}
sub arity ($sub) {
$signatures::arities{$sub+0};
}
}
subtest "signatures" => sub {
package _test1 {
sub foo ($xxx, $yyy) {
}
sub bar ($zzz, $aaa=1) {
}
sub baz ($zzz, @rest) {
}
}
is_deeply signatures::args(_test1->can('foo')), [qw(
$xxx
$yyy
)];
is signatures::arity(_test1->can('foo')), 2;
is_deeply signatures::args(_test1->can('bar')), [qw(
$zzz
$aaa
)];
is signatures::arity(_test1->can('bar')), 1;
is_deeply signatures::args(_test1->can('baz')), [qw(
$zzz
@rest
)];
is signatures::arity(_test1->can('baz')), -2;
};
subtest "no signature subs" => sub {
package _test2 {
sub foo {
}
}
is signatures::args(_test2->can('foo')), undef;
is signatures::arity(_test2->can('foo')), undef;
};
TODO: subtest 'lexical_subs' => sub {
local $TODO = "FAIL";
use feature 'lexical_subs';
no warnings "experimental::lexical_subs";
state sub foo ($foo) {
}
is_deeply signatures::args(\&foo), [qw(
$foo
)];
is signatures::arity(\&foo), 1;
};
done_testing;
view raw 01-signatures-args.t hosted with ❤ by GitHub
diff --git a/toke.c b/toke.c
index 88524b4..b517e37 100644
--- a/toke.c
+++ b/toke.c
@@ -12432,6 +12432,48 @@ S_parse_opt_lexvar(pTHX)
return var;
}
+void
+Perl_parse_subsignature_remember_sub_arg(pTHX_ OP* var)
+{
+ HV* signaturesubmap;
+ SV* subid;
+ SV* name;
+ HE* signatures;
+ AV* list;
+
+ signaturesubmap = get_hv("signatures::subs", 0);
+ if (!signaturesubmap) signaturesubmap = get_hv("signatures::subs", GV_ADD);
+
+ subid = newSViv((IV)(PL_compcv));
+ name = newSVsv(PAD_COMPNAME_SV(var->op_targ));
+ signatures = hv_fetch_ent(signaturesubmap, subid, 0, 0);
+ if (signatures) {
+ list = (AV*)SvRV(HeVAL(signatures));
+ } else {
+ list = newAV();
+ }
+ av_push(list, name);
+ hv_store_ent(signaturesubmap, subid, newRV_inc((SV*)list), 0);
+}
+
+void
+Perl_parse_subsignature_remember_sub_arity(pTHX_ int min_arity, int max_arity) {
+ HV* signaturearitymap;
+ SV* subid;
+ int arity;
+
+ signaturearitymap = get_hv("signatures::arities", 0);
+ if (!signaturearitymap) signaturearitymap = get_hv("signatures::arities", GV_ADD);
+
+ subid = newSViv((IV)(PL_compcv));
+ if (max_arity == -1) {
+ arity = -(min_arity + 1);
+ } else {
+ arity = min_arity;
+ }
+ hv_store_ent(signaturearitymap, subid, newSViv(arity), 0);
+}
+
OP *
Perl_parse_subsignature(pTHX)
{
@@ -12488,7 +12530,10 @@ Perl_parse_subsignature(pTHX)
prev_type = 0;
min_arity = pos + 1;
}
- if (var) expr = newASSIGNOP(OPf_STACKED, var, 0, expr);
+ if (var) {
+ expr = newASSIGNOP(OPf_STACKED, var, 0, expr);
+ Perl_parse_subsignature_remember_sub_arg(var);
+ }
if (expr)
initops = op_append_list(OP_LINESEQ, initops,
newSTATEOP(0, NULL, expr));
@@ -12539,6 +12584,7 @@ Perl_parse_subsignature(pTHX)
initops = op_append_list(OP_LINESEQ, initops,
newSTATEOP(0, NULL,
newASSIGNOP(OPf_STACKED, var, 0, slice)));
+ Perl_parse_subsignature_remember_sub_arg(var);
}
prev_type = 2;
max_arity = -1;
@@ -12590,6 +12636,7 @@ Perl_parse_subsignature(pTHX)
newSVpvs("Too many arguments for subroutine"))))),
initops);
}
+ Perl_parse_subsignature_remember_sub_arity(min_arity, max_arity);
return initops;
}

やってることは signatures::subs みたいな名前でハッシュをつくって放りこんでるだけです。perl のコード難しくてあってるかわからないのと、たぶん参照カウントがおかしかったりしてそうですが、とりあえず簡単には動く。

ただ、lexical_subs に対してはうまくとれない (とってるサブルーチンのアドレスがちがう) ので、そこが TODO かな、というのと、サブルーチンとか大量に定義されるので、結構メモリ食ったりしそうだな、ってのが怪しい感じです。

  1. トップ
  2. tech
  3. perl 5.19.9 の signatures 構文に引数リストをとれる機能を足す

Perl 5.19.9 で実装された signatures の構文をためしてみる - tokuhirom blog を見てそんなのできたのか〜と思ったので、いろいろ試してみた。なんとこれは、今までになく直感的に引数が書けてしまう革命的構文です。

use v5.19;
use strict;
use warnings;
use feature 'signatures';
no warnings "experimental::signatures";

use feature 'lexical_subs';
no warnings "experimental::lexical_subs";

# 引数の数チェックをしてくれる。便利
eval {
	say "check length of arguments";
	state sub hello ($foo) {
		say "hello $foo";
	}


	hello();
	#=> Too few arguments for subroutine at pl.pl line 9.
}; if ($@) { warn $@ }

# デフォルト値も普通に書けます
eval {
	say "default value";
	state sub hello ($foo="world") {
		say "hello $foo";
	}


	hello();
	#=> "hello world"

	hello("yunotti");
	#=> "hello yunotti"
}; if ($@) { warn $@ }

# デフォルト値に式も書けます。Ruby なんかでも書けますね。引数として指定されない場合だけ氷菓されます
eval {
	say "expression in default value (evaluated same as Ruby)";
	my $i = 0;
	state sub hello ($foo=$i++) {
		say "hello $foo";
	}


	hello();
	#=> hello 0

	hello();
	#=> hello 1

	hello("yunotti");
	#=> "hello yunotti"

	hello();
	#=> hello 2
}; if ($@) { warn $@ }

# @foo を指定したら可変引数にできます。%hash とかもできます。
eval {
	say "variable length arguments";
	state sub hello ($foo, @rest) {
		say "hello $foo and " . join(", ", @rest);
	}


	hello("yunotti", "miyachan", "sae", "hiro");
	#=> hello yunotti and miyachan, sae, hiro
}; if ($@) { warn $@ }

# @_ の挙動は今までどおり変数渡しです。
eval {
	say 'with @_: @_ is passed by variable (same as prev perls)';
	state sub hello ($foo, $bar) {
		$foo = 'xxx';
		$_[1] = 'xxx';
	}


	my $foo = 'foo';
	my $bar = 'bar';
	hello($foo, $bar);
	say "$foo, $bar"; #=> foo, xxx

}; if ($@) { warn $@ }

ただ引数の名前とかは外からとることができない。せっかく構文に組込まれたのなら、とれてもよさそうだなと思った。すこしコード追ってみたけど、基本、今までで同じようなコードが内部的に生成されるだけっぽい。サブルーチンリファレンスに附属する形でなんかメタデータを入れれたらいいんだけど、よくわからなかった。

あと、Smart::Args みたいなのは使えるかと思って試してみたけど、既存コードと同じ感じなので、当然普通に使える。ただ、signatures を活用した感じにはできなそう。うまいやりかたあるのかな。

use v5.19;
use strict;
use warnings;
use feature 'signatures';
no warnings "experimental::signatures";

use feature 'lexical_subs';
no warnings "experimental::lexical_subs";


{
	# Smart::Args は普通に使える
	use Smart::Args;
	{
		# % を書けば呼ばれる時点でハッシュかどうかのチェックは入る
		state sub hello (%) {
			args(my $foo);
			say "hello $foo";
		}

		hello(foo => "foobar");
		eval {
			hello(1);
		}; if ($@) { warn $@ }; #=> Odd name/value argument for subroutine at pl.pl line 20.
	};

	{
		# % を書かなければ今までどおり Smart::Args 側でエラる
		state sub hello {
			args(my $foo);
			say "hello $foo";
		}
		eval {
			hello(1);
		}; if ($@) { warn $@ }; #=> Odd number of elements in anonymous hash at lib/site_perl/5.19.9/Smart/Args.pm line 39.
	}
};

{
	# せっかくなら signatures を活用したい感あるけどいい感じにならない (引数名を2度書かないといけない)

	use PadWalker qw/var_name/;
	sub validate ($var, $rule) {
		my $name = var_name(1, \$_[0]) =~ s{^\$}{}r;
		require Smart::Args;
		$_[0] = Smart::Args::_validate_by_rule($_[0], 1, $name, $_[1]);
	}

	{
		state sub hello ($foo) {
			validate($foo, { isa => 'Int' });
			say "hello $foo";
		}
		eval {
			hello("xxx");
		}; if ($@) { warn $@ }; #=> Validation failed for 'Int' with value xxx
	}
};
  1. トップ
  2. tech
  3. perl 5.19.9 の signatures 構文 (普通に引数を書ける構文) を試す

bare metal という言葉を最近知って、おもしろそうだなあと思ったので Raspberry Pi 上で試してみた。bare metal とは OS レスで直接動かす (つまりリッチなマイコン的に使う) ことのようです。bare metal で動かすだけだと目標が低い感じなので、まず mruby を動かすところまでやろう、という感じです。

yamanekko さんというかたが既にやって日本語で書いているので、それを見てコピペしながらやってみつつという感じです。ちょいちょいうまくいかなかったのでかなりハマった……

ARM クロスコンパイル環境の構築

https://launchpad.net/gcc-arm-embedded にあるバイナリをインストールするのが一番良い。これを入れれば以下にあるような fpu 問題にも悩まない。

大変めんどう。ググるといろいろでてくるけど、今流行りの全部うまいことやってくれる感じのスクリプトがあったので試してみた。

https://github.com/jsnyder/arm-eabi-toolchain

git clone git@github.com:jsnyder/arm-eabi-toolchain.git
cd arm-eabi-toolchain
make install-cross

大変時間がかかる。Makefile を開いて、--enable-languages="c,c++" となってるところから c++ を消したほうがいいっぽい。使わないし…

入るライブラリ

  • arm-none-eabi-
    • gcc
    • binutils (objdump とか)
    • gdb
  • GMP
    • 数値演算ライブラリ
  • MPFR
    • 浮動小数点ライブラリ
  • MPC
    • MPFR ベースのさらに複雑な浮動小数点ライブラリ
  • newlib
    • 組み込み環境用のライブラリ。 libc とかを提供

少し古いバージョンが入る

FPU 問題

最初

arm-none-eabi-ld: error: main.elf uses VFP register arguments, /Users/cho45/arm-cs-tools/arm-none-eabi/lib//libc.a(lib_a-errno.o) does not

とか出て ld が通らなかった。

yamanekko さんのレポジトリにある Makefile だと fpu ライブラリへのパスが書いてあるけど、上のスクリプトでクロスコンパイル環境を構築してもでてこない……

gcc の configure オプションか何かなのかもしれないけど、時間がかかるしむずかしいので諦めた。

よくわからなすぎるのでググったらARM EABIでの浮動小数点演算というが参考になった。

結局 -mfloat-abi=soft にして完全にソフトウェア命令しか出さないようにしたら動くようになった。-mfloat-abi=soft=softfp だと浮動小数点演算が走ったタイミングで死んだりしてよくわからない。

オプションの種類は arm-none-eabi-gcc --target-help で見ることができる。-mfpu=vfp のところもいろいろ種類があるけどよくわからない。

未だ完全には解決してない。

リンカスクリプト

memmap というファイルになってる。面倒だった。間違えるといろいろよくわかないことになる。今回遭遇したのは

  • .ARM.exidx を定義してなかったせいでおこられる
    • C++ 用のセクションらしいけど 特定の関数を使おうとすると必要っぽい
    • 定義の順番がおかしいとできるバイナリがデカくなったり、動かなくなったりする
  • アラインメントがおかしかったせい?で newlib の mALLOc が無限ループする
    • 当然ヒープを実装せず使わないなら再現しないので簡単なLチカだと気付かない

newlib が要求してくるシステムコール類は syscalls.c でモック的に定義されている(コピペしたのものをクリーンアップした)。だいたいはどうでもいい関数だけど、_sbrk が大変重要で、これによってヒープが実現されている。

メモリ割当のマッピングとかはリンカスクリプトで定義したものを vectors.s (アセンブラ) で C 側から見れるようにして、それを使ってヒープとかの実装をしているみたい。

gdb でシミュレーションデバッグ

.gdbinit に書いてあるけど

file main.elf
target sim
load

とすることで、gdb 上でシミュレーションして動かすことができるので、周辺機器が絡まない、とりあえずのデバッグはこれでも十分できる。

ただ、一部の演算、確認したのは % (余剰)で変なエラーがでておかしい。

実際やった手順

mruby なしでLチカ

まず mruby なしでLチカができないとどうしようもないので、まずそこまでできるプロジェクトツリーを作って動くことを確認するまでやった。

https://github.com/cho45/raspberrypi-bare-metal

この過程だけでも上記の通りかなりハマった……

mruby ありでLチカ

次に mruby ありでLチカに挑戦した。yamanekko さんのmruby-rs-led を参考に、Linux の gpio 的な GPIO module gem を実装した。

GPIO のアドレスとかはデータシート記載のものを手動で変換せずにできるだけそのまま書いておきたいと思うので、そうしてる。バスアドレスというのと、物理アドレスというのがあって、データシートにはバスアドレスで書いてあって、しかし実際使うのは物理アドレスなので、変換 (固定値) が必要みたい。

これによって以下のように書けるように (完全にちゃんと動いているか微妙だけど) なった。Ruby にすると一気に簡単になった感じがして落差がウケます。ほぼ Linux 上で適当にラッパー書いて書くのと同じコードになった。うまくラップできれば主要コードは Ruby レベルでテストが書けてよさそう。

pin = 16

GPIO.direction(pin, :low)
bool = false
loop do
	bool = !bool
	GPIO.write(pin, bool)
	10000.times { }
end

しかしここまで動かすのも大変で、何時間もハマった。ここでハマったのは特にリンカスクリプトでアラインメントがおかしかったせいで、mrb_open() がそもそも返ってこないというのがつらかった。

gdb で追ってもなぜか newlib の中で無限ループにになるので意味不明だった。

そしてそれがようやくなおったと思っても mrb_open() の最中に浮動小数点演算もしていて、ここでハングするということが起きてつらかった。最終的には上記の通り、とりあえず常にソフトウェアにしてなんとか動かした。

sleep の実装を書いてないので Ruby 側でビジーループしてる。16ピンにしてるのは、Raspberry Pi の基板上に実装済みの LED だからで、デバッグ用に新規配線をしなくてすむからです。

sleep の実装

さすがにビジーループをRuby側でやるのはダサいので次に sleep を実装した。

system timer と arm timer というのがあるけど、今回は system timer を使った。arm timer はシステムの状態によってクロックが変わるみたい。system timer は独立したクロックを持ってるようだ。

ただ、肝心の system timer clock がよくわからなかった。試行錯誤したかんじだと、1Mhz ぐらいで動いてる。分周比設定とかがなくて謎。要追試

Cレベルではビジーループしてる。うまくCPUスリープさせたいけど、まだよくわかってない。

mruby でハマったところ

mruby ではあまりハマらなかった。mrbgem 作るのは大変簡単になっていておもてなし感がある。

ただ、build_conf.rb で gem を指定せずにビルドすると、あとから gem を指定しても、clean するまで反映されないのが罠ぽかった。深くおってないけど、DISABLE_GEMS が永続されてしまうぽい?

この件は再現させて1行パッチ送ったらマージされたのでたぶん大丈夫です。build_config.rb を書きかえたら .c ソースのリビルドが走るはず…

レポジトリ

これも習作だけど別レポジトリにおいてる

課題点

  • fpu の問題の解決
  • GPIO モジュールの拡張
    • TIMER や割込みなどを使えるように
    • ハードウェア I2C SPI の実装
    • bit banging での I2C 実装
  • JTAG を試す
  • ブートローダーを使うように
    • SDカードいちいち抜き差ししてるとつらいので

感想

mruby は夢が広がる。mruby で OS とか作ったらかっこよさそう。

  1. トップ
  2. tech
  3. mruby を Raspberry Pi 上で bare metal で動かすまで