ヾノ*>ㅅ<)ノシ帳

ノンジャンルのふりーだむなブログです。

kozosのcross-gcc4でmipsアセンブリをコンパイルし、gdbのsimで実行する

mips書くのは久しぶりでいろいろミスっているかもしれない)

動作環境

kozosのVMイメージのcross-gcc4が入った方

アセンブリ

C言語で書くとこんな感じのアセンブリを書く

#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()を発行する方法が分かる。

https://sourceware.org/git/gitweb.cgi?p=binutils-gdb.git;a=blob;f=sim/mips/interp.c;h=9dbac8c58fc76f8a3e54c9819037c25bc0a495b6;hb=HEAD

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

スタートアップルーチン

プログラムを実行するためにまず初期化の処理をする。 コンパイラが自動でしてくれない以下の処理を自分で記述せねばならない。

  • スタックポインタの設定
  • レジスタの設定
  • BSSセクションの初期化(今回は変数を使用しないため割愛する)
  • main()などのメインの処理をする関数の呼び出し
  • main()終了後の処理

『熱血!アセンブラ入門』を参考に以下のスタートアップルーチンを書いた。 32ビットのスタックポインタの設定が一命令で完結しないのは即値が16ビットの幅しかないためである。

また、_startgccにスタートアップのシンボルとして認識してもらうために、_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と書く。 mipsjal(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の頁にあった。

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でも問題なく動く)。

完成したリンカスクリプトが以下のmips.ldsである。

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

参考文献


この辺の話が好きな方は組込み技術者向け「初めてのC言語」にアクセスするとよいだろう。 (ドメインから察するに名大の情報コースの高田研の人が書いたっぽい。さすが…)