概要
Arduinoのブートローダから起動するプログラムを作成し,シミュレータで動作を確認する.前提として,AVRの開発環境とgdbをインストールしておく必要がある.
以下やったこと.
- startup.sだけ作成してミュレータで実行する
- リンカスクリプトを作成し,startup.sだけ実行
- main.cを加え,startup.sからmainを呼び出す
- main.cからstartup.sにアセンブラで書かれている関数を呼び出す
startup.sだけ作成してシミュレータで実行
以下のプログラムをアセンブリ言語で作成
.section .text
.global start
.type start, @function
start:
mov r16, r16
ldi r20, 0x2 // 即値をr20に代入
ldi r17, 0x1
lds r18, 0x40 //0x40番地の値をr18に代入
add r18, r17 // r17をr18に加えてr18に保存
sts 0x40, r18 // r18の値を0x40番地に格納
lds r19, 0x40
mov r16, r16
このプログラム自身に特に意味はなく,どうやってアセンブリ言語でプログラムを書いてシミュレータで実行するのかを確認する
以下のようにMakefileを作成する.PREFIXなどはAVRの開発環境と合わせる必要がある.
TARGET = startup
SRC = $(TARGET).s
BINARY = a.out
PREFIX = avr-elf
BASEDIR = /usr/core/$(PREFIX)
BINDIR = $(BASEDIR)/bin
AS = $(BINDIR)/$(PREFIX)-as
NM = $(BINDIR)/$(PREFIX)-nm
OBJDMP = $(BINDIR)/$(PREFIX)-objdump
GDB = $(BINDIR)/avr-gdb
all: $(SRC)
$(AS) -mmcu=avr5 $(SRC)
dump: a.out
$(OBJDMP) -d $^
sym: a.out
$(NM) $^
run: a.out
$(GDB) $^
clean:
rm -rf a.out
-
make
でコンパイル -
make dump
でコンパイルしたプログラムをobjdumpでディスアセンブル -
make sym
でシンボルテーブルの出力 -
make run
でgdbを使って実行
以下,それぞれの実行結果
>make dump
a.out: file format elf32-avr
Disassembly of section .text:
00000000 <start>:
0: 00 2f mov r16, r16
2: 42 e0 ldi r20, 0x02 ; 2
4: 11 e0 ldi r17, 0x01 ; 1
6: 20 91 40 00 lds r18, 0x0040
a: 21 0f add r18, r17
c: 20 93 40 00 sts 0x0040, r18
10: 30 91 40 00 lds r19, 0x0040
14: 00 2f mov r16, r16
ディスアセンブルの結果です.
>make sym
00000000 T start
nmの結果です.
>make run
/usr/core/avr-elf/bin/avr-gdb a.out
GNU gdb (GDB) 7.11
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "--host=x86_64-apple-darwin14.5.0 --target=avr".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from a.out...(no debugging symbols found)...done.
(gdb) target sim
Connected to the simulator.
(gdb) load
Loading section .text, size 0x16 lma 0x0
Start address 0x0
Transfer rate: 176 bits in <1 sec.
(gdb) break *0x0
Breakpoint 1 at 0x0
(gdb)
(gdb) run
Starting program: /Users/hiroaki/home/Samples/Arduino/src/assem/a.out
Program received signal SIGTRAP, Trace/breakpoint trap.
0x00000002 in start ()
(gdb) disas
Dump of assembler code for function start:
0x00000000 <+0>: mov r16, r16
=> 0x00000002 <+2>: ldi r20, 0x02 ; 2
0x00000004 <+4>: ldi r17, 0x01 ; 1
0x00000006 <+6>: lds r18, 0x0040
0x0000000a <+10>: add r18, r17
0x0000000c <+12>: sts 0x0040, r18
0x00000010 <+16>: lds r19, 0x0040
0x00000014 <+20>: mov r16, r16
End of assembler dump.
(gdb) info register
r0 0x0 0
r1 0x0 0
r2 0x0 0
r3 0x0 0
r4 0x0 0
r5 0x0 0
r6 0x0 0
r7 0x0 0
r8 0x0 0
r9 0x0 0
r10 0x0 0
r11 0x0 0
r12 0x0 0
r13 0x0 0
r14 0x0 0
r15 0x0 0
r16 0x0 0
r17 0x0 0
r18 0x0 0
r19 0x0 0
r20 0x0 0
r21 0x0 0
r22 0x0 0
r23 0x0 0
r24 0x0 0
r25 0x0 0
r26 0x0 0
r27 0x0 0
r28 0x0 0
r29 0x0 0
r30 0x0 0
r31 0x0 0
SREG 0x0 0
SP 0x0 0x0 <start>
PC2 0x2 2
pc 0x2 0x2 <start+2>
(gdb) x/16bx 0x0
0x0 <start>: 0x00 0x2f 0x42 0xe0 0x11 0xe0 0x20 0x91
0x8 <start+8>: 0x40 0x00 0x21 0x0f 0x20 0x93 0x40 0x00
(gdb) x/16bx 0x1
0x800001: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x800009: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
gdb) si
0x00000004 in start ()
(gdb) disas
Dump of assembler code for function start:
0x00000000 <+0>: mov r16, r16
0x00000002 <+2>: ldi r20, 0x02 ; 2
=> 0x00000004 <+4>: ldi r17, 0x01 ; 1
0x00000006 <+6>: lds r18, 0x0040
0x0000000a <+10>: add r18, r17
0x0000000c <+12>: sts 0x0040, r18
0x00000010 <+16>: lds r19, 0x0040
0x00000014 <+20>: mov r16, r16
ここでは,以下の処理をしています.
- gdbで作成したプログラム(a.out)を実行(読み込み?)
-
(gdb)target sim
でシミュレータで実行を指定 -
(gdb)load
でプログラムを読み込み -
(gdb)break *0x0
でプログラムメモリの0x0番地にブレイクポイントを設定 -
(gdb)run
で実行.0x0番地で止まる. -
(gdb)disas
でgdbに読み込んだプログラムのディスアセンブル.現在実行する位置に矢印がある. -
(gdb)info register
でレジスタの値を表示 -
(gdb)x/16bx 0x0
で,プログラムメモリの0x0からバイト単位(b)で値を16進(x)で16回繰り返して表示する.なお,10進で表示する場合はx
の代わりにd
. -
(gdb) x/16bx 0x1
で,データメモリの0x0からバイト単位(b)で値を16進(x)で16回繰り返して表示する.なお,アドレスが0x800001となっているのは,gdbではデータメモリを表す場合0x800000がアドレスに加えられて表示される.実際はデータメモリの0x1番地となる.恐らく,プログラムメモリかデータメモリかを指定して表示させることはできない.プログラムメモリの0x1以降を見たい場合は,必ず0x0を指定してその代わり長さを256とかにして見るしかない(2016/8/24現在). -
(gdb)si
でステップ実行.その後disasで本当に1ステップ実行したことを確認
P.S データメモリに0x80000が加えられるのは,gdbではなくてリンカの問題らしい(https://www.microchip.com/webdoc/AVRLibcReferenceManual/mem_sections_1sec_dot_data.html)
ここまでのプログラムは,以下の環境で試すことができる
>git clone https://github.com/hiro4669/iosv.git
>cd iosv
>git branch st_ver1 origin/st_ver1
>git checkout st_ver1
>cd startup
>make
リンカスクリプトを作成し,startup.sだけ実行
これまではstartup.sをavr-elf-asでアセンブリしたバイナリを実行していたが,これをavr-elf-gccを使ってコンパイルするようにMakefileを修正する.また,メモリマップを調整するためのリンカスクリプトを導入し,startup.sをコンパイルして実行する.
OUTPUT_FORMAT("elf32-avr")
OUTPUT_ARCH(avr)
ENTRY("start")
SECTIONS
{
. = 0x0;
.text : {
*(.text)
}
.rodata : {
*(.strings)
*(.rodata)
*(.rodata.*)
}
.data : {
*(.data)
}
.bss : {
*(.bss)
*(COMMON)
}
}
PREFIX = avr-elf
BASEDIR = /usr/core/$(PREFIX)
BINDIR = $(BASEDIR)/bin
AS = $(BINDIR)/$(PREFIX)-as
NM = $(BINDIR)/$(PREFIX)-nm
CC = $(BINDIR)/$(PREFIX)-gcc
LD = $(BINDIR)/$(PREFIX)-ld
OBJDMP = $(BINDIR)/$(PREFIX)-objdump
GDB = $(BINDIR)/avr-gdb
OBJS = startup.o
TARGET = tos
CFLAGS = -Wall -nostdinc -nostdlib -fno-builtin
CFLAGS += -mmcu=avr5
CFLAGS += -g
LFLAGS = -static -T ld.scr -L.
.SUFFIXES: .c .o
.SUFFIXES: .s .o
all: $(TARGET)
$(TARGET): $(OBJS)
$(CC) $(OBJS) -o $(TARGET) $(CFLAGS) $(LFLAGS)
cp $(TARGET) $(TARGET).elf
.c.o : $<
$(CC) -c $(CFLAGS) $<
.s.o : $<
$(CC) -c $(CFLAGS) $<
dump: $(TARGET).elf
$(OBJDMP) -d $^
sym: $(TARGET).elf
$(NM) $^
run: $(TARGET).elf
$(GDB) $^
clean:
rm -rf $(OBJS) $(TARGET) $(TARGET).elf
startup.sは変更せず,先の例と同じようにmake run
などを実行して正しく実行できていることを確認する.
ここまでのプログラムは,以下の環境で試すことができる
>git clone https://github.com/hiro4669/iosv.git
>cd iosv
>git branch st_ver2 origin/st_ver2
>git checkout st_ver2
>cd startup
>make
main.cを加え,startup.sからmainを呼び出す
以下に示すmain.cを作成し,startup.sから呼び出してみる.Arduino UnoはAtmega328pというMCUで動作する.Arduino UnoはこのAtmega328pに独自のブートローダをインストールした状態でArduinoのブートローダがROMに書き込まれた状態で出荷され,ユーザプログラムを呼び出すようになっている.また,このブートローダはアプリケーションプログラムをROM貼り付けたあと,リセットベクタと割り込みベクタの位置を変更し,0x0番地にリセットベクタ,0x2番地から割り込みベクタが設定されるようにMCUの設定を書き換える(詳しくはデータシート参照).
そして,これらの処理が終わると,PCを0に設定し,割り込みベクタにジャンプします.そのため,通常はその後に続く割り込みベクタを避け,スタートアップルーチンにジャンプすることでアプリケーションプログラムをスタートします.具体的には以下の様なコードを記述します.
reset: jmp start
jmp reset
jmp reset
jmp reset
start:
mov r0, r0
ldi r28, 0x2
このソースコードををコンパイルし,make run
して0x0でブレイクポイントを設定すると,以下のようにうまく動きません.
(gdb) break *0
Breakpoint 1 at 0x0: file startup.s, line 6.
(gdb) run
Starting program: /Users/hiroaki/home/Samples/Arduino/src/boot/tos.elf
Program received signal SIGTRAP, Trace/breakpoint trap.
0x00000002 in reset () at startup.s:6
6 reset: jmp start
(gdb) disas
Dump of assembler code for function reset:
0x00000000 <+0>: jmp 0x4 ; 0x4 <start>
End of assembler dump.
(gdb) si
Program terminated with signal SIGILL, Illegal instruction.
The program no longer exists.
ここでは,(gdb)si
とステップ実行を試みても,プログラムが終了したと言われてしまいます.どうやら,gdbのシミュレータはjmp
命令を使うとこうなるようです.
また,例えばstartのラベルのアドレスでブレイクしようとしても,0x0以外のアドレスはデータメモリだと認識されてしまい,ブレイクポイントを設定できません.そこで,C言語で書くプログラムの行番号を指定してブレイクポイントを仕込むとします.そして,そのブレイクポイントからステップ実行することで,そこまで書いたプログラムが正しく実行できていることを確認します.
C言語で書いたプログラムを呼び出す前に,最低限やらなければならないのはスタックポインタの設定です.Atmega328pのデータシートにも,アプリケーションプログラムの起動時にスタックポインタを設定するよう書かれています.Atmega328pのデータシートによると,データメモリは2KBでなので,0x0000から0x800までで,先頭256バイト(0x100)はレジスタやIO,拡張IOのアドレスとされているので,実際に使えるのは0x100から0x800までです.そこで,スタックポインタの初期値をほぼデータメモリの最後尾,0x7fcとして設定します.ただし,gnuではデータメモリは0x800000が加えられて表現されるので,0x8007fcとなります.
この設定を行うため,リンカスクリプト,およびstartup.sを変更します.
OUTPUT_FORMAT("elf32-avr")
OUTPUT_ARCH(avr)
ENTRY("start")
SECTIONS
{
. = 0x0;
.text : {
*(.text)
}
.rodata : {
*(.strings)
*(.rodata)
*(.rodata.*)
}
.data : {
*(.data)
}
.bss : {
*(.bss)
*(COMMON)
}
. = 0x8007fc;
.bootstack : {
_bootstack = .;
}
}
最後の数行が書き加えた部分で,オフセットを変更し,その後にシンボル'_bootstack'を追加しています.
次はstartup.sです.
.equ TEXT, 0x1122
.section .text
.global start
.type start, @function
reset: jmp start
start:
mov r0, r0
ldi r28, lo8(_bootstack)
ldi r29, hi8(_bootstack)
out 0x3d, r28
out 0x3e, r29
jmp call_main
call_main:
rcall main
このプログラムはリンカスクリプトで0x0に配置されるので,jmp start
がリセットベクタの位置に配置されることになり,まずこのコードが実行されます.これはstartねのジャンプ命令なので,そのままstartにジャンプします.次に,ldi
とlo8
,hi8
,_bootstackを使って_bootstackのアドレスの上位1バイトをr29に代入,下位1バイトをr28に代入し(メモリ空間は2バイト空間なので),それをout命令で0x3dと0x3eに代入します.このアドレスにはスタックポインタ(SP)が割り当てられているので,これでスタックポインタを設定したということになります.
その後,call_mainにジャンプして,そこからrcall
命令でmainにジャンプします.このmainはC言語で書かれた関数です.以下に示します.
int main(void) {
char a = 1;
return (int)a;
}
これをコンパイルし,リンクするためにMakefileを書き換えます.
-OBJS = startup.o
+OBJS = startup.o main.o
OBJSにmain.oを書き足すだけです.
コンパイルし,make dump
してみます
Disassembly of section .text:
00000000 <reset>:
0: 0c 94 02 00 jmp 0x4 ; 0x4 <start>
00000004 <start>:
4: 00 2c mov r0, r0
6: c2 e0 ldi r28, 0x02 ; 2
8: d1 e0 ldi r29, 0x01 ; 1
a: cc ef ldi r28, 0xFC ; 252
c: d7 e0 ldi r29, 0x07 ; 7
e: cd bf out 0x3d, r28 ; 61
10: de bf out 0x3e, r29 ; 62
12: 0c 94 0b 00 jmp 0x16 ; 0x16 <call_main>
00000016 <call_main>:
16: 00 d0 rcall .+0 ; 0x18 <main>
00000018 <main>:
18: cf 93 push r28
1a: df 93 push r29
1c: 0f 92 push r0
1e: cd b7 in r28, 0x3d ; 61
20: de b7 in r29, 0x3e ; 62
22: 81 e0 ldi r24, 0x01 ; 1
24: 89 83 std Y+1, r24 ; 0x01
26: 89 81 ldd r24, Y+1 ; 0x01
28: 99 27 eor r25, r25
2a: 87 fd sbrc r24, 7
2c: 90 95 com r25
2e: 0f 90 pop r0
30: df 91 pop r29
32: cf 91 pop r28
34: 08 95 ret
0x0にresetが配置され,0x4にジャンプ,さらにcall_mainにジャンプしてrcall
でmainを呼び出していることが分かります.
main run
してmainの1行目にブレイクポイントを設定します.
GNU gdb (GDB) 7.11
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "--host=x86_64-apple-darwin14.5.0 --target=avr".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from tos.elf...done.
(gdb) target sim
Connected to the simulator.
(gdb) load
Loading section .text, size 0x3c lma 0x0
Start address 0x4
Transfer rate: 480 bits in <1 sec.
(gdb) break 1
Breakpoint 1 at 0x28: file main.c, line 1.
(gdb)
break 1
で設定しています.(gdb)run
します
(gdb) run
Starting program: /Users/hiroaki/home/Samples/Arduino/src/boot/tos.elf
Program received signal SIGTRAP, Trace/breakpoint trap.
0x0000002a in main () at main.c:3
3 char a = 1;
(gdb) disas
Dump of assembler code for function main:
0x0000001e <+0>: push r28
0x00000020 <+2>: push r29
0x00000022 <+4>: push r0
0x00000024 <+6>: in r28, 0x3d ; 61
0x00000026 <+8>: in r29, 0x3e ; 62
0x00000028 <+10>: ldi r24, 0x01 ; 1
=> 0x0000002a <+12>: std Y+1, r24 ; 0x01
0x0000002c <+14>: ldd r24, Y+1 ; 0x01
0x0000002e <+16>: eor r25, r25
0x00000030 <+18>: sbrc r24, 7
0x00000032 <+20>: com r25
0x00000034 <+22>: pop r0
0x00000036 <+24>: pop r29
0x00000038 <+26>: pop r28
0x0000003a <+28>: ret
End of assembler dump.
すると,0x28のldi r24, 0x01
で止まり,PCが1つずれて0x2aで止まっていることが確認できます.
さらに,(gdb)info register
でレジスタの値を見てみます.
(gdb) info register
r0 0x0 0
r1 0x0 0
r2 0x0 0
r3 0x0 0
r4 0x0 0
r5 0x0 0
r6 0x0 0
r7 0x0 0
r8 0x0 0
r9 0x0 0
r10 0x0 0
r11 0x0 0
r12 0x0 0
r13 0x0 0
r14 0x0 0
r15 0x0 0
r16 0x0 0
r17 0x0 0
r18 0x0 0
r19 0x0 0
r20 0x0 0
r21 0x0 0
r22 0x0 0
r23 0x0 0
r24 0x0 0
r25 0x0 0
r26 0x0 0
r27 0x0 0
r28 0xf7 247
r29 0x7 7
r30 0x0 0
r31 0x0 0
SREG 0x0 0
SP 0x8007f7 0x8007f7
PC2 0x2a 42
pc 0x2a 0x2a <main+12>
これをみると,スタックポインタが0x8007f7であり.先ほど設定したスタックポインタは0x7fcだったので5バイトずれています.これは,PCの保存で2バイト,mainの先頭でpushを3回実行しているので,合計5バイトスタックに積んでいることが予想でき,辻褄が合いそうです.
また,詳細は省略しますが,この後(gdb)si
でステップ実行していくと,C言語の関数を抜けたところでstartup.sに戻ることが確認できます.
ここまでのプログラムは,以下の環境で試すことができる
>git clone https://github.com/hiro4669/iosv.git
>cd iosv
>git branch st_ver3 origin/st_ver3
>git checkout st_ver3
>cd startup
>make
P.S gcc-4.8.5で試したところ,mainでpushはしないバイナリが出力されました.