H8/3048のプログラム実行コードと変数のメモリへの割付について

01Feb2004
10Nov2002
Copyright(C) TNCT Kosaka

1.はじめに
この文書はアセンブリで書かれたスタートアップルーチン,Cプログラムの関数,変数がどのようにメモリに割り付けられるかを一般的に述べたものです。初心者向け開発環境においても一般的に当てはまります。

PCなどでのCプログラミングでは,すべてのプログラムや変数がRAM上で実行されているため,メモリへの割付については通常意識しなくて済みます。
しかしマイクロコンピュータでは変数がROM領域に割り当てられると,その変数の値を変更しようとしても変更することが出来ません。変数はRAM上に割り当てたいのですが,電源を切ると値を保つことが出来ないので,初期値を与えることが出来ません。
例えば,次のようなCプログラムを考えましょう。

int b=0;

void func(void)
{
    static int si=0;
    :

もし,グローバル変数bやスタティック変数siがROM領域に割り当てられてしまうと,これら2つの変数の値は0のまま変更することが出来ません。
このようなことが起こらないようにするのがリンカスクリプトとスタートアップルーチンの役割です。
スタートアップルーチンはコンパイラがROM領域に置いた初期値(この例では0)をRAM領域の変数にコピーして,ユーザのプログラムからはRAM領域に置かれた「初期値を持つ変数」が使えるようにします。

2.予備知識
2.1 RAMとROM

メモリは,特別な方法で書き込むことの出来,電源を供給しなくても書き込んだ値を保持するROM(read only memory)と,CPUの命令で書き込むことが出来るRAM(random access memory)の2種類に分けられます。H8ではフラッシュメモリがROMの役割を果たします。

ROM:特別な方法で書き込みます。CPUからのプログラム中の通常命令で書き込むことはできません。電源を供給しなくても書き込んだ値を保持します。プログラムの機械語コードはROMに書き込んで欲しいですし,変数の中で,変数の値が変更されないものは,ROMに書き込んであってもかまいません。
RAM:CPUからのプログラム中の通常命令で書き込むことができます。電源が供給されなくなると記憶していた内容が消えてしまいます。プログラムによって変更される変数はRAM領域に割り当てられているべきです。

2.2 変数の領域割り当て
Cプログラムで書かれた変数は大きく分けて次の3つになります。
(1)オート変数 これはRAM領域の末尾部分のスタック領域に,プログラムが起動した後に生成され,使われなくなったら消滅します。
(2)初期化されていないグローバル変数 これはRAM領域に割り当てられます。
(3)変化しない定数 これはROM領域に割り当てることが出来ます。
(4)初期化されたグローバル変数,初期化されたstatic変数 これらの変数はRAM領域に置かれますが,初期値がROM領域に書かれ,スタートアップルーチンで初期値がRAM領域の変数にコピーされ,RAM領域の変数として有効に働くようになります。

Cプログラムの部分 (例)

int a;                                 *1
int b=123;                             *2
static int c=111;                      *3
const int cval[]={1,2,4,8,16,32};      *4

void func(void)
{
    int x=0;                           *5
    static int s=100;                  *6
    char ch1[]="abcdefg";              *7
    char *pch="ANCDEFG";               *8
    const char[]="1234567890";         *9
    b++;
    c--;
   
}

main()
{
    int y;                            *10
    int vect[]={1,1,1};               *11
    const int cvect[]={1,0,0};        *12
    a=0;
   
}

*1:これはRAM領域に割り当てます。
*2,*3,*6:初期値がROM領域に書かれ,スタートアップルーチンで初期値をRAM領域にコピーされ,RAM領域で変数として使います。(*3のstatic修飾は,マルチソースファイルプログラミング時に他のソースファイルから見えないという意味です。)
*4,*9,*12:ROM領域に割り当てます。
*5,*10:これはRAM領域の末尾部分のスタック領域に,プログラムが起動した後に生成され,使われなくなったらこの変数は消滅します。(例えば*5は関数funcが起動した時に生まれ,関数から戻る時に消滅します。)
*7:文字列"abcdefg"はROM領域に書かれます。文字列"abcdefg"が実行時にスタック領域にコピーされ,使われます。関数終了時には消滅します。
*8:"ANCDEFG"はROM領域に書かれます。関数起動時に,スタック領域に生成されたポインタ変数pchに,ROM領域にかかれている"ANCDEFG"の先頭アドレスが代入されます。
*11:{1,1,1}はROM領域に書かれます。{1,1,1}が実行時にスタック領域にコピーされ,使われます。関数終了時には消滅します。

もし,*2,*3,*6において,これらの変数がROM領域に割り当てられたままなら,変数の値を変更することが出来ません。また,もしこれらの変数がRAM領域に割り当てられたなら,電源を切った時,初期値の保持が出来ません。
スタートアップルーチン中に初期値をRAM領域にコピーし,変数を二重化するからくりが必要です。

3.コンパイラでの領域割付
コンパイラでは,プログラムコードや変数をRAM,ROM領域へ割付けることをしません。そのかわりセクションという複数の領域に割付けます。「H8/3048F用Cコンパイラユーザーマニュアル」によれば,次のようになっています。セクションの名前はコンパイラが自動的に付けてしまいます。

−−−−−−−−
「ROM,RAMの割り付け」
プログラムをROM化する場合は,静的な領域を以下のようにROMとRAMに割り付けます。
プログラム領域  (セクション.text)→ ROM
定数領域     (セクション.text)→ ROM
初期化データ領域 (セクション.data)→ RAM(初期値はROMに配置される)

未初期化データ領域(セクション.bss)→  RAM
−−−−−−−−

プログラム領域にはプログラムの機械語コードが置かれます。
定数領域とは,*4,*9,*12の値や,"abcdefg","ANCDEFG",{1,1,1}であり,最終的にROM領域に割り付けられていても差し支えない内容です。
未初期化データ領域とは*1の変数です。最終的にはRAM領域に割り付けられる予定です。
初期化データ領域とは,*2,*3,*6のように変数が二重化した領域です。

4.リンカでのメモリ割付と実行コードへの実アドレス付与
メモリアドレスでどこからどこまでROMがあり,どこからどこまでRAMがあるかはCPUのメモリマップから読み取ることが出来ます。

例えばH8/3048シングルチップアドバンストモード(モード7,AKI-H8/3048)では
ROM:0x00000-0x1FFFF (128kbyte)
RAM:0xFEF10-0xFFF0F (  4kbyte)
となっています。
なお,
ROM領域の0x00000-0x000FFまでは割り込みベクタテーブルとなっています。

ここでリンカスクリプトによりメモリアドレスへの割り当てを行います。

0x00100から始まるROM領域にセクション.textと初期値を連続して割り当てます。
0xFEF10から始まるRAM領域にセクション.data,セクション.bssを連続して割り当てます。

リンカスクリプトにより,

int b=123;

ではセクション.text内の「ある場所(ここのアドレスは「_dataROM_begin」
)」に123が置かれます。ここで「ある場所」が「_dataROM_begin」から6バイト目だったとすると,プログラム中の「b++;」の命令ではセクション.dataの先頭から6バイト目の変数がb++されるような機械語実行コードになります。
しかし,このままプログラムを実行すると,セクション.dataの先頭から6バイト目の変数には123が入っておらず,プログラマの予定した動作と異なってしまいます。
初期値のおかれた場所の各変数をセクション.dataにコピーする作業を行なうのはスタートアップルーチンの仕事になります。

5.スタートアップルーチンでのスタックポインタの設定と初期値のおかれた場所からセクション.dataへのコピー

スタートアップルーチンで行なう事は次のとおりです。

(1)スタックポインタの設定
(2)ROM上の初期値領域をセクション.data(RAM上の初期化済変数領域)へのコピーする
(3)セクション.bss(RAM上の初期化されていない変数領域)に0を埋める
   これは初期化されていない変数に初期値0を与えるおせっかいである
(4)main()関数を呼び出す

    /*スタックポインタの初期化*/
    mov.l    #_initial_stack_point,sp

    /*初期値のある書き換えデータ領域のコピー。ROM->RAM。*/
    mov.l    #_dataROM_begin,er0
    mov.l    #_dataRAM_begin,er1
    mov.l    #_dataRAM_end,er2
    bra .mvdata1
.mvdata:
    mov.b    @er0,r3h
    mov.b    r3h,@er1
    adds    #1,er0
    adds    #1,er1
.mvdata1:
    cmp.l    er2,er1
    blo      .mvdata

    /*初期値のない書き換えデータ領域のクリア*/
    mov.b    #0,r0h
    mov.l    #_bss_begin,er1
    mov.l    #_bss_end,er2
    bra      .cldata1
.cldata:
    mov.b    r0h,@er1
    adds     #1,er1
.cldata1:
    cmp.l    er2,er1
    blo      .cldata

    /*main()をコール*/
    jsr      @_main

    /*終了処理は無限ループ*/
_forever:
    bra     _forever