マイクロコンピュータにおけるプログラム実行コードと変数のメモリへの割付について
H8/3664の場合

10Nov2002
Copyright(C) coskx

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領域にコピーするからくりが必要です。

3.コンパイラでの領域割付
コンパイラでは,プログラムコードや変数を RAM,ROM領域へ割付けることをしません。そのかわりセクションという複数の領域に割付けます。「H8/3048F用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の初期値がROMに置かれます。

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

例えばH8/3644シングルチップノーマルモードでは
ROM:0x0000-0x7FFF (32kbyte)
RAM:0xF780-0xFF7F ( 2kbyte)
となっており,ROM領域の0x0000-0x0033までは割り込みベクタテーブルとなっています。
0x0034-0x00FFに絶対アドレス指定プログラム(アセンブリ言語)エリアとしています。

ここでリンカへの次の2つのコマンドにより,セクションのメモリアドレスへの割り当てが完了します。
(1) ROM (D,X)
ここでセクション名XはDと同じサイズのセクションです。コンパイラはセクションDに対し各変数の初期値を置きます。機械語実行コード では,セクションDに置かれた変数があたかもセクションXにあるようにアドレスが埋め込まれます。(機械語実行コード中のアドレスはリンカの段階で決定し ます。)
(2) START P,C,D(34),X,B(0F780)
0x0034から始まるROM領域にP,C,Dを連続して割り当て,0x0F780から始まる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'FF80,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のような変数の値として,2つのセクションの先頭アドレスを得ることが出来るようになっている。

    .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


スタートアップルーチンで,このような事前の作業が行なわれているので,ユーザのCプログラムは安心して,RAM領域の変数を使用できるようになったわけです。