2017年04月23日

ラズパイのベアメタル開発:circle と newlib を併用する

 ラズパイのベアメタル開発。先日も書いたけど、ラズパイ3で動かすのに手こずっていた。何とかできたみたいなので、手順を書いておきます。

 まず、ラズパイ1の開発に使っていた Maccasoft さんのカーネルは、ラズパイ2以上を使うことは想定されていない。ペリフェラルのアドレスを書き換えて、スタートアップも書き換えれば何とかなるんだと思うけど,ちょっと自分の力では無理だったので、別のカーネルを探すことにした。

 採用したのは circle。ラズパイ1・2・3・Zero で動作確認されている。ただし、標準ライブラリを一切使わない想定で書かれているので、少し込み入ったプログラムを書こうとすると不自由を感じる。そこで、newlib と併用することを検討した。

 今回は、ツールチェインとして ARMDeveloper サイトで提供されているものを使った。"6-2017-q1-update" というもので、GCC のバージョンは 6.3.1。インストールして、実行パスを通しておく。

 Circle の Makefile を参考にして、リンクフェーズのコマンドを下のようにしてみた。リンカを ld から gcc にして、-nostartfiles を指定し、circle の startup.o をリンクする。

$ arm-eabi-none-gcc -o kernel7.elf -nostartfiles -Wl,-Map,kernel7.map \
-T $(CIRCLEHOME)/circle.ld $(CIRCLEHOME)/lib/startup.o $(OBJS) $(LIBS) -lm

 いろいろつまづきました。最初に引っかかったのは、「__aeabi_idiv が二重定義」というエラー。これは libgcc.a にあるのだが、circle/lib/libstub.S でも定義されている。無視してくれないんだ。さらに調べてみると、libgcc.a では同じオブジェクトファイルに __aeabi_idiv__aeabi_idivmod が定義されていて、後者が circle では定義されていないため、このオブジェクトファイルをリンクしようとして、定義済みの __aeabi_idiv とぶつかっていることが判明した。Circle の __aeabi_idiv を削るのはいろいろ問題を起こしそうだったので、libstub.S に以下の記述を書き加えて、Daruma BASIC のプロジェクトに追加することにした。

  .globl  __aeabi_idivmod
__aeabi_idivmod:
  push {r0, r1, lr}
  bl __aeabi_idiv
  pop {r1, r2, lr}
  mul r3, r2, r0
  sub r1, r1, r3
  bx lr

 ARM のアセンブラなんて使うの初めてだよ。なんかどきどきする。剰余の計算としてはあまり効率良くない感じだけど、とりあえずは動かすことが優先だ。

 次に遭遇したのが、uint8_t などが二重定義になっている、というエラー。これは、circle の stdint.h と newlib の stdint.h がぶつかっていることが原因。よく見ると、circle の stdint.h は全く使われていないので、_stdint.h などとリネームすれば良かった。

 次は、このエラー。

error: kernel7.elf uses VFP register arguments, /usr/local/(...中略...)
/lib/libm.a(lib_a-s_ceil.o) does not

 どうも、circle のビルドの時に指定している -mfloat-abi=hard があかんらしい。いろいろ検討したが、結局 -mfloat-abi=softfp に切り替えることにした。速度が少し落ちるのかも知れないけど、ここも動かすことを優先。

 次に、malloc() 系の処理。Newlib では _sbrk() をユーザーに提供させて、それを使って malloc() 系の関数を実装しているのだが、circle は自前でメモリ管理を行っているので、そちらを使うようにする。_sbrk() は 0 を返すようにして、circle の malloc()/free() をリンクすればよい。calloc(), realloc() は circle では実装されていないので、malloc() を使って書いておく。また、newlib は内部で _malloc_r(), _free_r(), _calloc_r(), _realloc_r() というリエントラント対応の関数を持っていて、これらも上書きしておかないと、circle の実装と newlib の実装を混用することになり、問題が起きる。(参考:https://sourceware.org/ml/newlib/2000/msg00143.html "user-defined malloc"

 そういうわけで、こんな感じで実装。

void *calloc(size_t count, size_t size)
{
  void *p = malloc(count * size);
  if (p != NULL)
    memset(p, 0, count * size);
  return p;
}

void *realloc(void *ptr, size_t size)
{
  void *p = malloc(size);
  if (p != NULL) {
    memmove(p, ptr, size);
    free(ptr);
  }
  return p;
}

struct _reent;
void *_malloc_r(struct _reent *r, size_t size)
{  return malloc(size); }

void _free_r(struct _reent *r, void *ptr)
{  free(ptr); }

void *_calloc_r(struct _reent *r, size_t count, size_t size)
{  return calloc(count, size); }

void *_realloc_r(struct _reent *r, void *ptr, size_t size)
{  return realloc(ptr, size); }

 また、strtoul() も二重定義になることがわかった。これも、_strtoul_r() というリエントラント可能なバージョンがあって、こちらをリンクしようとしてこけるらしい。そこで、_strtoul_r() もダミー実装を追加した。

unsigned long _strtoul_r(struct _reent *r, const char *s, char **ptr, int base)
{  return strtoul(s, ptr, base); }

 これで、単純なプログラムは動作することを確認したのだが、実際に Daruma BASIC をビルドしてみると、動作しない。しかも、途中で止まるとかそういうのではなくて、最初から立ち上がりもしない。いろいろコードを変更して調べていたとき、標準ライブラリの strcmp() をリンクすると立ち上がらなくなることがわかった。kernel7.map を調べて、原因が判明。strcmp() 由来の .eh_frame セクションがあるのだが、__init_start ラベルがその前にある。__init_start は、静的オブジェクトのコンストラクタを呼ぶのに使われているので、.eh_frame セクションの中身をコンストラクタのアドレスだと思って呼び出していることになる。そりゃ暴走するわな。というわけで、リンカスクリプトを修正して、.init_array の先頭が __init_start になるようにした。

--- circle/circle.ld	2017-04-23 19:12:35.000000000 +0900
+++ DarumaBasic/build_raspi_bm/circle.ld	2017-04-23 22:00:45.000000000 +0900
@@ -18,9 +18,9 @@
 
-	__init_start = .;
 
 	.init_array : {
+	__init_start = .;
 		*(.init_array*)
+	__init_end = .;
 	}
 
-	__init_end = .;
 

 一応これで、circle と newlib は共存できるようになったのかな。細かい不具合のあぶり出しはこれからだけど。

タグ:Raspberry Pi
Posted at 2017年04月23日 22:28:13
email.png