部内向けスキルアップ研修
「組込みOS自作入門」
2013年8月
目次
• はじめに • ブート・ローダー • シリアル経由でのファイル転送 • XMODEMを実装する(もくもく会) • アセンブラ・プログラミングはじめに
前回やったこと
• ROM、RAM、自動変数、静的変数、データ領
域に関する学習
今回やること
• ブート・ローダー
• XMODEMプロトコル
• アセンブラ・プログラミング
4.1 ブート・ローダー
4.1.1 ブート・ローダーの必要性 • これまでは、作成したプログラムをフラッシュ ROMへ書き込んでROM上で実行してきたが、 実はROMは書き込み回数に上限がある。 →メーカー保証は100回程度 (通常利用の範囲ならば1000回程度) • 電源OFFしても消えないので、電源ONと同時 に起動するOSが作成できるが、短所がある。4.1.1 ブートローダの必要性
• そこで、OSの実行形式ファイルを直接フラッ シュROMに書き込む代わりに、OSの実行形式 ファイルをシリアル経由でダウンロードし、そ れをRAM上に展開して起動するプログラムを フラッシュROMに書き込む ↓ ブート・ローダー(boot loader) • ブート・ストラップ(bootstrap) 電源ONでブート・ローダーを起動し、ブー ト・ローダーでOSをダウンロードしてRAM上 でOSを展開し起動するという2段構成4.1.1 ブートローダの必要性
• 良いところ – 気軽にOSのダウンロードが繰り返せる – 効率よく開発をすすめることができる • 悪いところ – RAMは電源OFFにすると内容が消えるので、 起動の度にOSをダウンロードしなければなら ない。4.1.1 ブートローダの必要性
• ROM化 – 開発中はブートローダーを利用してRAM上に 展開して起動し、製品化の段階でフラッシュ ROMに書き込んでROM上から起動する • 今回はOSのROM化まではやらない • OSをダウンロードしてRAM上で展開し、起動す るブート・ローダーを作成する4.2 シリアル経由でのファイル転送
• ブート・ローダーの機能 – シリアル経由でOSの実行形式ファイルをダウ ンロードし、RAM上に一旦保存 – 保存した実行形式ファイルを、RAM上に展開 – RAM上に展開したOSを実行 • シリアル通信には、XMODEMプロトコルを使用 する4.2.1 XMODEMプロトコル仕様
• 送信側 1. 受信側から定期的に送信されるNAKを受け たら、送信を開始する 2. ブロック単位でデータ送信 3. ACKが返ってきたら次を送信、NAKが返っ てきたら再送 4. データ終わりはEOTを送信し、ACKが返っ てきたら終了 5. 中断したい場合はCANを送信し、CANを受 信したら中断4.2.1 XMODEMプロトコル仕様
• 受信側 1. 準備ができたらNAKを送信し、受信開始。 2. SOHを受けたら、ブロックとして受信。 成功したらACKを返し、失敗したらNAKを 返す。 3. EOTを受けたらACKを返して終了。 4. 中断したい場合はCANを送信し、CANを受 信したら中断。4.2.1 XMODEMプロトコル仕様
フィールド サイズ 意味
a 1バイト SOH (Start Of Header) b 1バイト ブロック番号 c 1バイト チェック。ブロック番号を反転。 d 128バイト データ部。空きはEOFで埋める。 e 1バイト データ部のチェックサム。データ 部を256で割った余り a b c d e XMODEMのブロック・フォーマット
4.2.1 XMODEMプロトコル仕様
• ACK(ACKnowledge)
– 受信成功時の応答として送信されるコード • NAK(NegativeAcKnowledge)
XMODEMデータフロー概略図
受信側 送信側 → NAK → (送信開始を要求) ← SOH ← (ブロック開始) ← 01 ← (ブロック番号) ← FE ← (ブロック番号を反転させたもの) ← ・・・ ← (実際のデータ 128 バイト) ← sum ← (チェックサム) → ACK → ← SOH ← (ブロック開始) ← 02 ← (ブロック番号) ← FD ← (ブロック番号を反転させたもの) (上記手順を、データがなくなるまで繰り返す) ← EOT ← (転送終了) → ACK →4.3 XMODEMを実装する
• もくもく会 – 今回追加するファイル xmodem.h, xmodem.c – 今回修正するファイル main.c コマンド動作を実装 ld.scr バッファ領域を追加 serial.h,serial.c 文字の受信を実装 lib.h, lib.c ライブラリ関数を追加 Makefile4.3 XMODEMを実装する
•
手順
1. 前回のフォルダ「3.2」をコピーして「4.1」 を作成
2. ファイルの追加・修正 3. make → make image
4. ディップスイッチを書き込みモード (1,1,0,1)に設定後、make write 5. Tera termを起動して、ディップスイッチを 実行モード(1,0,1,0)に設定後、電源ON 6. リセットボタンを押すとプロンプト「kzload > 」が出るので、「load」を入力
4.3 XMODEMを実装する
•
手順
6.リセットボタンを押すとプロンプト「kzload > 」が出るので、「load」を入力 7.直ちに、メニューから、ファイル-転送-XMODEM-送信を選択する。ファイル選択 ウィンドウが開くので、とりあえず、 「defines.h」を転送してみる。 8.間に合わなかった場合は、再度、リセットボタ ンを押してやり直す。 9.完了したら、dumpコマンドを使って、転送し たファイルを確認4.3 XMODEMを実装する
•
手順
10. cygwinで転送元ファイルを表示し、転送し たファイルを比較 $ hexdump –c defines.h 11.転送データはブロック単位で送信されるた め、受信したファイルは余った部分が0x1aで 埋められている事を確認して下さい。 (XMODEMの仕様。気にしない!)アセンブラ・プログラミング
• OSを自作する場合、アセンブラ(assembler)
の知識は必須
→「スタート・アップ」「割り込みの入口と
と出口」「スレッドのディスパッチ」の3箇
所はアセンブラでないと書けない!
• とりあえず、処理の「目的」を意識して、内
容にアタリをつけて読むと理解しやすい
(全部命令を覚える必要はない)
4.5.1 スタック
• C言語でプログラムを書くとき、すべての変
数を静的変数にするのはムダ
→ 常時メモリを占有し続けるのはムダ
• 関数に入った時のみ獲得され、returnによっ
て関数から抜けるときには捨てられ、メモリ
領域が使いまわせる「自動変数」が必要
4.5.1 スタック
•
効率よくメモリを利用する手順
1. ある程度の容量の領域を予め確保 2. 現在、領域のどこまでを利用しているかを示す ポインタを用意 3. 関数呼び出しにより自動変数のための領域が必 要になった場合には、必要分だけポインタをず らす 4. さらに関数呼び出しされた場合には、更にポイ ンタを必要分だけずらす 5. 関数から戻るときには、ポインタを戻す4.5.1 スタック
•
スタックを管理するために利用されるポイ
ンタをスタック・ポインタ
•
関数単位でスタック上に確保される領域を
4.5.2 アセンブラ
• CPUはメモリ上にある機械語命令を逐次実行して いく • しかしメモリ上には数値しか保存できない • したがって、機械語命令とは数値のことで、数値 が命令としての意味を持っている4.5.3 CPUのレジスタ
• レジスタとは、CPU内部にある記憶領域のこと • CPUは加算や減算などの数値計算を行うための回 路を持っているが、これらの回路の入力や出力は レジスタに接続 • CPU(RISC)は、メモリ上の値を直接操作できず、 CPU上のレジスタに読み込んでから処理4.5.3 CPUのレジスタ
• 「c = a + b」の演算の場合 1. 変数aが配置されている位置のメモリの値を、レジスタ 1に読み込む 2. 変数bが配置されている位置のメモリの値を、レジスタ 2に読み込む 3. レジスタ1とレジスタ2を加算し、結果をレジスタ3に格 納する 4. レジスタ3の値を、変数cが配置されている位置のメモ リに書き込む4.5.3 CPUのレジスタ
• メモリ上の値をレジスタに読み込むことをロード • レジスタの値をメモリ上に書き込むことをストア • メモリ上のデータのロードやストア、演算など汎
4.5.4
プログラム・カウンタ(PC)
• 汎用レジスタとは別の重要なレジスタで、「プロ グラム・カウンタ(program counter)」がある – CPUが現在実行中の命令アドレスを指す (正確には、次の命令のアドレス) • CPUはPCを加算しつつPCの指すメモリ先の命令を 逐次実行しながら処理を進めていく – PCは次の命令を指す位置まで自動で加算される • PCの役割は、いわゆるジャンプ命令 (C言語でいうところのgoto)4.5.5
ニーモニックとアセンブラ
• オペコードとオペランド – アセンブリの命令を表す部分をオペコードと呼ぶ。命令 に対する引数に相当する部分をオペランドと呼ぶ – 機械語を数値で表すのは人間には読みづらいので、適当 な単語を使って人間がわかりやすいようにしたのがニー モニック – c = a + b の機械語プログラム例 01 01 80 00 → 1番レジスタに変数a(アドレス:0x8000)の値をロード 01 02 80 04 → 2番レジスタに変数a(アドレス:0x8004)の値をロード 03 03 01 02 → 1番レジスタと2番レジスタを加算し3番レジスタに格納 02 03 80 10 → 3番レジスタの値を変数c(アドレス:0x8010)にストア4.5.5
ニーモニックとアセンブラ
• ニーモニック – 0x01は「ロード」を行う機械語命令なので、「ld」 – 0x02は「ストア」を行う機械語命令なので、「st」 – 0x03は「加算」を行う機械語命令なので、「add」 – 以上のように定義した場合の、c = a + b の機械語プ ログラムのニーモニック表記 ld r1, 0x8000 → 1番レジスタに変数a(アドレス:0x8000)の値をロード ld r2, 0x8004 → 2番レジスタに変数a(アドレス:0x8004)の値をロード add r3, r1, r2→ 1番レジスタと2番レジスタを加算し3番レジスタに格納 st r3,0x8010 → 3番レジスタの値を変数c(アドレス:0x8010)にストア4.5.5
ニーモニックとアセンブラ
• インストラクション – 機械語の命令はインストラクションとも呼ばれる – どの数値がどの命令として動作するかという決まりを、 「命令セット」「インストラクション・セット」と呼ぶ – 命令セットはCPUごとに違う (H8の命令セットとPentium系の命令セットは異なる)4.5.5
ニーモニックとアセンブラ
• アセンブリ言語、アセンブル – ニーモニックで表した機械語プログラム表記をアセンブ リ言語という – アセンブリ言語で書いたコードを機械語コードに変換す る作業をアセンブルという – アセンブル変換するプログラムをアセンブラと呼ぶ – 機械語コードをアセンブラに逆変換することを逆アセン ブルと呼ぶ4.5.6
H8のアセンブラ
• 実際にアセンブラを読んでみよう – 方法はいくつかあるが、今回は最後に生成された実行 形式ファイルを逆アセンブルしたものを確認する • 手順 – 先程、作成したフォルダ「4.1」をコピーして「4.2」 フォルダを作成 – lib.cファイルに関数を追記(P.155 リスト4.13) – main.cに関数呼び出しを追記(P.155 リスト4.14) – make を実行 – Cygwinで以下のコマンドを実行 $ /usr/local/h8300-elf/bin/objdump –d kzload.elf逆アセンブルの結果
• 0000010c <_main>: – main関数の先頭 • 000004f8 <_func>: – func()関数の先頭 • 00000162 jsr @0x4f8:24 – func()の呼び出し箇所だろうと想像できる ニーモニックの細かい文法は気にせず、想像で読 んでいくことがコツ逆アセンブルの結果
• 0000015a: mov.w #0x2, r1 – r1レジスタに2を代入 • 0000015e: mov.w #0x1, r0 – r0レジスタに1を代入 • H8はR0~R7という16ビットレジスタを8個持っ ている。拡張レジスタとしてE0~E7という16 ビットレジスタがあり、これらを組み合わせて ER0~ER7という32ビットレジスタとして利用 することができる(long型整数やポインタ)main()関数部
• 79 01 00 02 mov.w #0x2, r1 – 「79」はオペコード、後ろ3バイトがオペランドで、 1バイト目は値の格納先レジスタ、後は代入値 – R1に0x0002という値を代入 • ビッグ・エンディアンとリトル・エンディアン – 「0x0002」を格納するとき、ビッグ・エンディアン は「0x00」「0x02」、リトル・エンディアンは「0x02」、 「0x00」とひっくり返して格納する – CPUによって異なり、H8はビッグ・エンディアン。 Pentium系CPUはリトル・エンディアン。近年の多く はビッグ・エンディアン。main()関数部
• イミディエイト値(即値)
– オペランドにレジスタに代入する「0x0002」という値 が、そのまま記述
func()関数部
• mov.l er6, @-er7
– H8ではER7がスタック・ポインタとして利用される – スタック・ポインタを減算してスタック4バイト領域 を確保してスタック・ポインタの指す先にER6の値を 格納(ストア)する – ER6が上書きされてしまうので、ER6の値(内容)を スタックに退避している – スタックを獲得するのにER7の値を減算している。ア ドレス値が少なくなる方向に伸びること(下方伸長) – 「@er7」レジスタの値をアドレス値として、アドレ スが指す先のメモリの意味(レジスタ間接) →メモリ参照の方法をアドレッシング・モード
func()関数部
• アドレッシング・モード H8は8種類のアドレッシング・モードを持つ No 、 アドレッシングモード、 記号 (1) レジスタ直接 Rn (2) レジスタ間接 @ERn (3) ディスプレースメント付きレジスタ間接 @(d:16, ERn)/@ (d:24, ERn) (4) ポストインクリメントレジスタ間接@ERn+ プリデクリメントレジスタ間接@-ERn (5) 絶対アドレス @aa:8/@aa:16/@aa:24 (6) イミディエイト #xx:8/#xx:16/#xx:32 (7) プログラムカウンタ相対 @(d:8,PC) /@(d:16, PC) (8) メモリ間接 @@aa:8func()関数部
• アドレッシング・モード – 「@-er7」レジスタの加減算とメモリ・アクセスを1 命令で同時に行う – スタック操作に2命令が利用されると、その命令の間 で割り込みが入りスタック操作された時に、スタック の整合性が取れなく可能性がある – スタック操作は1命令で行える必要があるfunc()関数部
• mov.l er7,er6 – スタック・ポインタであるER7の値をER6にコピーし ている – ER6はフレーム・ポインタと呼ばれる使われ方をして おり、スタック・フレームの先頭を指している – ER6もER7も、ポインタとして利用されるため、アド レスを保持する必要があるので、32ビットレジスタ が利用される。func()関数部
• subs #4, er7 – スタック・ポインタであるER7をさらに4バイトだけ 減算して、4バイトの領域を確保 – func()の内部で利用している自然変数「c」の領域 – 「#4」の4は定数値を表すイミディエイト値(即値)func()関数部
• add.w r1,r0
– R0とR1の値を加算して、R0に代入 – 「a +b 」の処理(1 + 2)をしている
• mov.w r0, @(0xfffe:16, er6)
– 加算結果はR0に格納されているが、自動変数「c」 はスタック上に存在
– フレーム・ポインタを減算してアドレスを計算して、 そこに加算結果を代入
func()関数部
• mov.w @(0xfffe:16, er6), r0
– スタック上に格納されている変数「c」の値をr0に代 入することでモリチの準備をし、R0を関数の戻り値 とする – 元々、R0に「c」の値が入っていたからムダな処理 – 「c」をvolatile定義したため、最適化処理が行われ ていない – 「加算して、結果をスタックに保存して、戻り値を準 備する」という一連の処理が最適化されていない
func()関数部
• adds #4, er7
– スタック・ポインタを4バイト加算することで、自動 変数「c」の為に確保していたスタックを開放
• mov.l @er7+, er6
– スタック上に対比されていたER6の値をER6に読み込 んで(ロード)、ER6を元に戻す – ロード後にスタック・ポインタであるER7は4バイト 加算 • rts – スタック上から戻り先アドレスを取得し、ジャンプし て関数の呼び出し元に戻る