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

Copyright(C) 20Feb2004
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変数 これは初期値が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領域の末尾部分のスタック領域に,プログラムが起動した後に生成され,使われなくなったら消滅します。
*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の初期値がROMに置かれます。

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

例えばSH2/7045(モード2,AKI-SH2/7045)では
内部ROM:0x00000000-0x0004FFFF (256kbyte)
外部RAM:0x00400000-0x0041FFFF (1024kbyte)
内部RAM:0xFFFFF000-0xFFFFFFFF ( 4kbyte)
となっている。
なお,
ROM領域の0x00000-0x00280までは割り込みベクタテーブルとなっています。

ここでリンカスクリプトによりメモリアドレスへの割り当てが完了します。

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

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

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()関数を呼び出す

!-----------------------------------------------------------------------------
! ROMに格納されているデータ(変数など)をRAM領域にコピーする
!   RAM_BGN  : データを格納するRAM領域の開始アドレス
!   RAM_END : データを格納するRAM領域の終了アドレス
!   ROM_BGN : 実際にデータが書き込まれているROM領域の開始アドレス
!-----------------------------------------------------------------------------
    MOV.L       RAM_BGN,  r0        ! RAM_BGN -> r0
    MOV.L       RAM_END,  r1        ! RAM_END -> r1
    MOV.L       ROM_BGN,  r2        ! ROM_BGN -> r2
    BRA         LOOP11
    NOP
LOOP1:
    MOV.B       @r2,    r3          ! ROM領域からデータを取得
    MOV.B       r3,     @r0         ! RAM領域へデータをコピー
    ADD         #1,     r0          ! RAM領域を指すアドレスをインクリメント
    ADD         #1,     r2          ! ROM領域を指すアドレスをインクリメント
LOOP11:
    CMP/eq      r0,     r1          ! RAM_BGN == RAM_END ならば T=1
    BF          LOOP1               ! T!=1 ならば LOOP1へ
    NOP
END_LOOP1:

!-----------------------------------------------------------------------------!
!   初期化無しのグローバル変数を0で初期化
!   BSS_BGN  : 初期化無しのグローバル変数が格納されている開始アドレス
!   BSS_END : 初期化無しのグローバル変数が格納されている終了アドレス
!-----------------------------------------------------------------------------
    MOV.L       BSS_BGN,  r0        ! BSS_BGN -> r0
    MOV.L       BSS_END,  r1        ! BSS_END -> r1
    BRA         LOOP21
    NOP
LOOP2:
    MOV         #0,     r3          ! r3=0
    MOV.B       r3,     @r0         ! グローバル変数の領域に0を代入
    ADD         #1,     r0          ! 領域を指すアドレスをインクリメント
LOOP21:
    CMP/eq      r0,     r1          ! BSS_BGN == BSS_END ならば T=1
    BF          LOOP2               ! T!=1 ならば LOOP2へ
    NOP
END_LOOP2: