検索
連載

フルスクラッチの“Hello World”の仕組みを見てみようH8マイコンボードで動作する組み込みOSを自作してみよう!(2)(2/3 ページ)

今回は、前回動作させた「Hello World」のソースコードについて解説し、組み込みソフトウェアでのHello Worldの動作の仕組みを見ていきます。その中身は、意外と奥深いものです。

Share
Tweet
LINE
Hatena
※本記事はアフィリエイトプログラムによる収益を得ています

3.3.リンカスクリプト

 リンカスクリプトは、読者の方々にはあまりなじみのないファイルだと思います。また、名前くらいは聞いたことがあっても、実際に触れたことはないという方も多くいることと思われます。

 リンカスクリプトは、リンカがオブジェクトファイルをリンクして実行形式を作成する際に、オブジェクトのメモリ配置を指定するためのファイルです。

 これは、実は普通のアプリケーションプログラムのリンク時にも必要なのですが、通常はリンカにビルトインされているデフォルトのリンカスクリプト(注1)が勝手に利用されるため、プログラマ側で指定する必要はありません。このため、OS上で動作する通常のアプリケーションプログラムを書いている場合には、とくに意識する必要はありません。

※注1:汎用OSでは、仮想メモリシステムにより複数のアプリケーションプログラムを同一のアドレス上で動作させることが可能なので、リンカスクリプトが共通化されています。


 しかし、組み込みソフトウェア開発では、メモリ調整もプログラマ側の責任で行う必要があるため、リンカスクリプトの読み書きが必要になる場合も多々あります。

 リンカスクリプトは、表1の「ld.scr」というファイルです。内部ではリスト1のような記述があります。

…(中略)…
 
SECTIONS
{
        . = 0x0;
 
        .vectors : {
                vector.o(.data)    ★vector.oを0x0に配置する
        }
 
…(後略)…
リスト1 リンカスクリプト(ld.scr)

 リスト1は、“「vector.o」というオブジェクトファイルをアドレス「0x0」に配置する”という意味になります。

 「vector.o」は、「vector.c」をコンパイルすることで作成されます。「vector.c」はリスト2のようになっています。

#include "defines.h"
 
extern void start(void);        /* スタートアップ */
 
/*
 * 割り込みベクターの設定
 * リンカスクリプトの定義により、先頭番地に配置される
 */
void (*vectors[])(void) = {
  start, NULL, NULL, NULL, NULL, NULL, NULL, NULL,    ★リセットベクターにはstart()のアドレスを設定する
 
…(後略)…
リスト2 割り込みベクターの設定(vector.c)

 よって、「vector.c」で定義されている配列「vectors[]」が、割り込みベクターに設定されます。リセットベクターは、先頭の「vectors[0]」に相当しますが、リスト2では、「vectors[0]」には関数「start()」のアドレスが初期値として設定されています。これにより、電源ON時には「start()」という関数から実行が開始されることになります。

3.4.スタートアップ

 ここまでで、リセットベクターの設定により、電源ON直後に関数「start()」から実行が開始されることが分かりました。

 それでは、C言語で「start()」という関数を作成しておけば、電源ONでCPUはその関数から実行を開始してくれるのでしょうか?

 実は、そうはうまくいきません。正確にいうと、実行を開始すること“だけ”はできるのですが、正常に動作を継続することができないのです。

 というのは、C言語の関数というのは内部で「スタック」を利用しているため(注2)、スタックポインタが初期化されていなければ、関数呼び出しが正常に動作しないからです。もしも、スタックポインタの初期化を行わずに関数呼び出しを行った場合、おかしなアドレスを勝手にスタックとして利用してしまい、暴走して固まったり勝手に再起動するなどしてしまうでしょう。

※注2:「スタックを使える」という前提で、コンパイラはスタックを使うようなアセンブラコードを生成します。


 他にも静的変数領域の初期化など、C言語で書いたプログラムを動かすにはさまざまな前処理が必要です。リセットベクターから呼ばれる割り込みハンドラでは、これらの初期化を行い、その後にC言語の関数を呼ぶようにすれば、後は、C言語でプログラムを記述することができます。例えば、初期化後に「main()」関数を呼ぶようにしておけば、あたかも通常のCプログラムのように「main()」から処理が始まるものとしてプログラムを記述できるわけです。

 このような初期化を行う部分を「スタートアップ」と呼びます。スタートアップでは、レジスタの初期化などC言語では記述できないような処理を行う必要があるため、通常はアセンブラで記述します。

 今回作成した「Hello World」のスタートアップは、表1の「startup.s」というファイルです。その内容は、リスト3のようになっています。ラベル名が「_start」になっているため、関数「start()」として(注3)リセットベクター(vector.cのvectors[0])に登録され、電源ON時にはここから動作を開始することになります。

        .h8300h
        .section .text
        .global _start
#       .type   _start,@function
_start:
        mov.l   #0xffff00,sp    ★スタックポインタの定義
        jsr     @_main          ★main()を呼び出す
 
1:
        bra     1b
リスト3 スタートアップ(startup.s)

※注3:先頭にアンダースコア(_)が付加されています。C言語の関数は、シンボル作成時に先頭にアンダースコアが付加されるため、C言語上の「start()」はアセンブラ上では「_start」となります。


 また、リスト3ではスタックポインタの初期化を行い、「main()」関数(アセンブラではアンダースコアが付加され、「_main」になります)を呼び出しています。このため、必要な処理を「main()」関数に記述すれば、「main()」から始まるような通常のC言語のイメージでプログラムが書けるわけです。

 「main()」関数は、表1の「main.c」で定義されています。「main.c」の内容はリスト4のようになっています。「puts()」により「Hello World」を出力して、後は無限ループにより、動作を停止するという簡単なものです。

#include "defines.h"
#include "serial.h"
#include "lib.h"
 
int main(void)                ★メイン関数
{
  serial_init(SERIAL_DEFAULT_DEVICE);
 
  puts("Hello World!\n");     ★「Hello World!」を出力する
 
  while (1)                   ★無限ループで動作停止する
    ;
 
  return 0;
}
リスト4 main()関数(main.c)

Copyright © ITmedia, Inc. All Rights Reserved.

ページトップに戻る