kozosのcross-gcc4でmipsアセンブリをコンパイルし、gdbのsimで実行する
(mips書くのは久しぶりでいろいろミスっているかもしれない)
動作環境
kozosのVMイメージのcross-gcc4が入った方
アセンブリ編
#include <unistd.h> int main(){ write(1, "Hello World\n", 12); // stdout = 1 return 0; }
愚直にmipsのアセンブリに落とし込むとこんな感じだろうか。いろいろ足りないので順を追って埋めていこう。
.data hello: .asciiz "Hello World\n" .text # write(1, hello, 12); lw $ra, 4($sp) # restore return address
write()を書く
write()関数はmipsで書くとどんな感じなのだろうか。 とりあえず今回はgdbのsimで動かすのでソースを見てwrite()のアセンブリを調べる。
『熱血!アセンブラ入門』を読むとsim向けにmipsのアセンブリを書いてみたがあるのでそれを参考にすると、 sim/mips/interp.cあたりを読めばシミュレータ向けのwrite()を発行する方法が分かる。
1162 /* Simple monitor interface (currently setup for the IDT and PMON monitors) */ 1163 int 1164 sim_monitor (SIM_DESC sd, 1165 sim_cpu *cpu, 1166 address_word cia, 1167 unsigned int reason) 1168 { 1176 reason >>= 1; 1181 switch (reason) 1182 kan++>STM32F>リンカスクリプト](http://www.usamimi.info/~mikanplus/stm32f/linker_script.htm){ ... 1203 case 8: /* int write(int file,char *ptr,int len) */ 1204 { ... 1216 } ... 1254 case 17: /* void _exit() */ 1255 { 1256 sim_io_eprintf (sd, "sim_monitor(17): _exit(int reason) to be coded\n"); 1257 sim_engine_halt (SD, CPU, NULL, NULL_CIA, sim_exited, 1258 (unsigned int)(A0 & 0xFFFFFFFF)); 1259 break; 1260 }
swtich-case に入ったときに reason が 8 になったときにwrite()
ができる。
sim_monitor()
を呼び出す命令列の条件を調べよう。
1783 if ((instruction & RSVD_INSTRUCTION_MASK) == RSVD_INSTRUCTION) 1784 { 1785 int reason = (instruction >> RSVD_INSTRUCTION_ARG_SHIFT) & RSVD_INSTRUCTION_ARG_MASK; 1786 if (!sim_monitor (SD, CPU, cia, reason)) 1787 sim_io_error (sd, "sim_monitor: unhandled reason = %d, pc = 0x%s\n", reason, pr_addr (cia)); 1788 1789 /* NOTE: This assumes that a branch-and-link style 1790 instruction was used to enter the vector (which is the 1791 case with the current IDT monitor). */ 1792 sim_engine_restart (SD, CPU, NULL, RA); 1793 }
(instruction & RSVD_INSTRUCTION_MASK) == RSVD_INSTRUCTION
がその条件のようだ。
マクロを展開するとこのようになる:instruction & 0xFC00003F == 0x39
75 #define RSVD_INSTRUCTION (0x00000039) 76 #define RSVD_INSTRUCTION_MASK (0xFC00003F)
なんだけど、これは嘘で、RSVD_INSTRUCTIONが5でないとVMの方で動作しないはず。 5が39になったのはこのコミットのせいらしい。
というわけで、正しくはinstruction & 0x3F == 0x5
。
ついでにinterp.cを調べると、 sim_monitorに入る前のreasonの値は、sim_monitorのswitch-case文のcaseの値をxとして
((x << 1) << RSVD_INSTRUCTION_ARG_SHIFT) & RSVD_INSTRUCTION_ARG_MASK
つまり
x << 7
に等しい。
78 #define RSVD_INSTRUCTION_ARG_SHIFT 6 79 #define RSVD_INSTRUCTION_ARG_MASK 0xFFFFF
というわけで、お望みの関数を呼びたいときはswitch-caseのcaseの値をxとして次の命令(instruction)を書けば良い:
.long (x << 7 | 0x5)
書くときは()内の式を計算してからにする。例えばwrite()の場合はx=8なので
.long (8 << 7) | 0x5
つまり .long 0x405
をアセンブリ命令として書くとwrite()をシミュレータで呼ぶことができる。
__write: # sim/mips/interp.c (case 8: write()) .long 0x405 # 8 << 7 | 0x5 = 0x400 | 0x5 - jr $ra nop
スタートアップルーチン
プログラムを実行するためにまず初期化の処理をする。 コンパイラが自動でしてくれない以下の処理を自分で記述せねばならない。
『熱血!アセンブラ入門』を参考に以下のスタートアップルーチンを書いた。 32ビットのスタックポインタの設定が一命令で完結しないのは即値が16ビットの幅しかないためである。
また、_start
をgccにスタートアップのシンボルとして認識してもらうために、_start
シンボルを.globl(.global)
ディレクティブでエクスポートした。
.globl _start _start: lui $sp, %hi(_estack) addiu $sp, $sp, %lo(_estack) jal main nop # daley slot move $a0, $v0 # exit(0)
残りのアセンブリを書く
mipsでは関数の引数はregister渡しである。第一引数は$a0、第二引数は$a1という具合だ。 write()を呼び出すアセンブリはこのようになる。
# in mips, function parameters are passed by registers li $a0, 1 la $a1, hello li $a2, 12 jal __write # write(stdout, "Hello World\n", 12)
mainが終了した後はexit()でシミュレーションを終了させたい。 exit()はこのように書けばシミュレータを終了させることができる。
_exit: # sim/mips/interp.c (case 17: _exit()) .long 0x885 # 17 << 7 | 0x5 = 0x880 | 0x005 jr $ra nop # delay slot
最後に気をつけたいのはリターンアドレスを関数を呼び出す前に保存し、関数から戻ったらそれを復元することだ。
mipsで関数funcを扱うとき、呼び出しはjal func
と書き、呼び出し元に戻る処理はjr $ra
と書く。
mipsのjal(jump and link)命令はリターンアドレスレジスタ$raに現在の$pcの値をセットする。
$raは呼び出し1回ぶんしか保存できないので、例えばmain()→write()と呼び出したとき、write()を呼び出しで$raが上書きされ、
main()でjr $ra
をしてmain()の呼び出し元に戻ることができなくなってしまう。
jr(jump register)命令はターゲットのレジスタに入っているアドレスにジャンプする命令である。
対策としてmain()で$raをスタックに保存&リストアする処理を書く(hello.s参照)。
完成したソースhello.sとし保存する。
.data hello: .asciiz "Hello World\n" .text .globl _start _start: lui $sp, %hi(_estack) addiu $sp, $sp, %lo(_estack) subu $sp, $sp, 4 sw $ra, 0($sp) jal main nop # daley slot lw $ra, 0($sp) addu $sp, $sp, 4 move $a0, $v0 # exit(0) _exit: # sim/mips/interp.c (case 17: _exit()) .long 0x885 # 17 << 7 | 0x5 = 0x880 | 0x005 jr $ra nop # delay slot __write: # sim/mips/interp.c (case 8: write()) .long 0x405 # 8 << 7 | 0x5 = 0x400 | 0x5 - jr $ra nop main: subu $sp, $sp, 4 # push stack sw $ra, 0($sp) # save return address # in mips, function parameters are passed by registers li $a0, 1 la $a1, hello li $a2, 12 jal __write # write(stdout, "Hello World\n", 12) lw $ra, 0($sp) # restore return address addu $sp, $sp, 4 # pop stack li $v0, 0 # return 0 jr $ra nop
リンカスクリプト編
さーて書けたぞーということでstdlibなしでコンパイルしたいところだが、_estak
の値(スタックポインタの初期位置)をコンパイルに教えねばならない。
また、メモリのマッピングをgdbのsimに合わせねばならない。
これを実現するのがリンカスクリプトである。
リンカスクリプトを書くために以下の情報が必要である。
- エントリポイントのアドレス
- スタック領域の終わりのアドレス(スタックはアドレスが小さい方に向かって伸びる)
情報を集める前にmipsのメモリマップの図が欲しいところだ。幸いWikipediaのR3000の頁にあった。
sim/mips/interp.cを探すと以下のアドレスの情報を見つけることができる。
121 /* Note that the monitor code essentially assumes this layout of memory. 122 If you change these, change the monitor code, too. */ 123 /* FIXME Currently addresses are truncated to 32-bits, see 124 mips/sim-main.c:address_translation(). If that changes, then these 125 values will need to be extended, and tested for more carefully. */ 126 #define K0BASE (0x80000000) 127 #define K0SIZE (0x20000000) 128 #define K1BASE (0xA0000000) 129 #define K1SIZE (0x20000000)
先ほどの図から察するに、プログラムはkナントカのスペースを使えば良さそうなので、
K0BASEの値をエントリポイント、K1BASE+K1SIZE-4を_estack
の値にする
(細かいことを気にしなければ_estackがK1BASEでも問題なく動く)。
ENTRY(_start) OUTPUT_FORMAT("elf32-bigmips", "elf32-bigmips", "elf32-big-mips"); /* sim/mips/interep.c: K1BASE + K1SIZE - 4 */ _estack = 0xBFFFFFFC; SECTIONS { /* sim/mips/interep.c: K0BASE */ . = 0x80000000; .text : { _ftext = . ; PROVIDE (eprol = .); *(.text) *(.text.*) } }
コンパイル編
stldlibなしで、先のリンカスクリプトを指定してコンパイルする。
[user@localhost mips]$ mips-elf-gcc -T mips.lds -nostdlib 1.s
実行
2通りで実行してみた。
sim_monitorのメッセージが嫌な人は回避策が『熱血!アセンブラ入門』に書かれているので購入をすすめる(ステマ)。
ヒントはsleep()
。
gdbのシミュレータに接続して実行する方法
一回目のr
で怒られたのはわざとやで((((;゚Д゚))))
[user@localhost mips]$ mips-elf-gdb -q a.out Reading symbols from /home/user/project/mips/a.out...(no debugging symbols found)...done. (gdb) target sim Connected to the simulator. (gdb) r Starting program: /home/user/project/mips/a.out warning: No program loaded. [Inferior 1 (process 42000) exited with code 057] (gdb) load Loading section .text, size 0x80 vma 0x80000000 Loading section .data, size 0xd vma 0x80000080 Start address 0x80000000 Transfer rate: 1128 bits in <1 sec. (gdb) r Starting program: /home/user/project/mips/a.out Hello World sim_monitor(17): _exit(int reason) to be coded [Inferior 1 (process 42000) exited normally] (gdb) q
runで実行する方法
[user@localhost mips]$ mips-elf-run a.out Hello World sim_monitor(17): _exit(int reason) to be coded
参考文献
- 熱血!アセンブラ入門 : 坂井 弘亮 : 本 : Amazon.co.jp
- X86アセンブラ/GASでの文法 - Wikibooks
- mikan++>STM32F>リンカスクリプト
- MIPS(4)
- MIPS Instruction Reference
この辺の話が好きな方は組込み技術者向け「初めてのC言語」にアクセスするとよいだろう。 (ドメインから察するに名大の情報コースの高田研の人が書いたっぽい。さすが…)