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

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/3048foneF用Cコンパイラユーザーマニュアル」によれば,次のようになっています。セクションの名前はコンパイラが自動的に付けてしまいます。

−−−−−−−−
「ROM,RAMの割り付け」
プログラムをROM化する場合は,静的な領域を以下のようにROMとRAMに割り付けます。
プログラム領域  (セクションP)→ ROM
定数領域     (セクションC)→ ROM
未初期化データ領域(セクションB)→ RAM
初期化データ領域 (セクションD)→ ROM,RAM
−−−−−−−−

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

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

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

ここでリンカへの次の2つのコマンドにより,セクションのメモリアドレスへの割り当てを行います。
(1)
ROM (D,X)
ここでセクション名XはDと同じサイズのセクションです。コンパイラはセクションDに対し各変数の初期値を置きます。機械語実行コードでは,セクションDに置かれた変数があたかもセクションXにあるようにアドレスが埋め込まれます。(機械語実行コード中のアドレスはリンカの段階で決定します。)
(2) START P,C,D(100),X,B(0FEF10)
0x00100から始まるROM領域にセクションP,セクションC,セクションDを連続して割り当てます。
0xFEF10から始まるRAM領域にセクションX,セクションBを連続して割り当てます。

この2つのコマンドにより,

int b=123;

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

5.スタートアップルーチンでのスタックポインタの設定とセクションDからセクションXへのコピー

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

(1)スタックポインタの設定
(2)セクションD(ROM上の初期値領域)をセクションX(RAM上の初期化済変数領域)へのコピーする
   (それぞれの先頭アドレスを知る方法はこの直後で説明)
(3)セクションB(RAM上の初期化されていない変数領域)に0を埋める
   これは初期化されていない変数に初期値0を与えるおせっかいである
(4)main()関数を呼び出す

    MOV.L    #H'FFF10,ER7    ;スタックポインタ設定

    ;move Section D to Section X
    MOV.L @_D_Head, ER0      ;source address to ER0
    MOV.L @_X_Head, ER1      ;destination address to ER1
    MOV.L @_D_Size, ER2      ;size to be copied to ER2
    OR.L  ER2,      ER2      ;(ER2 or ER2) to ER2
    JMP @LOOP_11
LOOP_1:
    MOV.B @ER0+,    R3H      ;source byte to R3H with ER0++
    MOV.B R3H,      @ER1     ;R3H to destination
    ADDS  #1,       ER1      ;increment destination address
    DEC.L #1,       ER2      ;ER2--
LOOP_11:
    BNE LOOP_1

    ;fill 0 to Section B
    MOV.L @_B_Head, ER1      ;destination address to ER1
    MOV.L @_B_Size, ER2      ;size to be copied to ER2
    MOV.B #0,       R3H      ;0 to R3H
    OR.L  ER2,      ER2      ;(ER2 or ER2) to ER2
    JMP   @LOOP_21
LOOP_2:
    MOV.B R3H,      @ER1     ;R3H to destination
    ADDS  #1,       ER1      ;increment destination address
    DEC.L #1,       ER2      ;ER2--
LOOP_21:
    BNE LOOP_2

    JSR @_main        ; Call main()
EternalLoop: BRA EternalLoop ;万が一戻ってきてもOK

ところで,セクションD(ROM上の初期値領域)をセクションX(RAM上の初期化済変数領域)へのコピーする時にはこれら2つのセクションの先頭アドレスとDセクションのサイズを知る必要がある。この3つの値はアセンブリ言語の拡張とリンカによる作業で,実行コードが出来上がった時に定数領域(ROM領域の一部)にしまわれている。
アセンブリ言語のスタートアップルーチンの末尾に次のように書いておくと,_D_Headや_X_Headのような変数の値として,3つのセクション(D,X,B)の先頭アドレスとセクションのサイズを得ることが出来るようになっていて,スタートアップルーチンで参照することができます。

    .SECTION    C,DATA,ALIGN=2
_D_Head:
    .DATA.L     (STARTOF D)     ; D Head Address
_X_Head:
    .DATA.L     (STARTOF X)     ; X Head Address
_D_Size:
    .DATA.L     (SIZEOF D)      ; D Size
_B_Head:
    .DATA.L     (STARTOF B)     ; B Head Address
_B_Size:
    .DATA.L     (SIZEOF B)      ; B Size
    .END