キーワード

セクション, オブジェクトファイルフォーマット, .text, プログラムローダ, ELF(Executable and Linkable Format)


これだけは覚えよう


原因と原理

私たちが日ごろ作っているプログラムは、ノイマン型コンピュータ上で動きます。ノイマン型コンピュータというのは、プログラムをメモリに蓄えているタイプのコンピュータ(Program stored computer)を指し、プロセッサとメモリを持ったものは大体ノイマン型です。当然私たちのプログラムもメモリにロードされて動いています。

プログラムが動くとき、さまざまな用途でメモリが使用されます。その用途は、機械語をおくためや、変数として値を格納しておくため、またC言語などでは関数呼び出しを行うためのスタックとして使用するため、mallocなどで動的に使うためなど、多岐にわたります。そして、それぞれ用途によってメモリにはいろいろと条件が課せられます。たとえば携帯電話やPCのBIOSなどの機械語をおくメモリは、リセットで消えてしまっては困るので、ROMにおかなければいけません。一方、変数をROMにおくと書き換えることができません。また変数でも設定などのデータは、普通のRAMでは電源を切ると消えてしまうので、バッテリバックアップ付のRAMやEEPROMやフラッシュROMなどにおく必要があります。そうなると、「ならすべてRAMにおけば問題ないや」と思うかもしれませんが、Intel PentiumやARM 720Tが持つMMU(メモリ管理ユニット)というユニットは機械語用のメモリとそれ以外を区別するので、やっぱり同じRAMでも区別しなければいけません。NECのV850Eプロセッサのように、プログラムコードとしてアクセスができるメモリ空間が制限されているものもあります (0x00000000-0x03ffffffの空間のみ)

ノイマン型コンピュータでプログラムを実行するためには、プログラムローダと呼ばれるプログラムが必要になります。プログラムローダは、トランスレータ(=コンパイラ+リンカ)が出力する実行形式ファイルの中にあるプログラムを、正しくメモリに配置しなければなりません。たとえば、機械語は消えないようFLASH ROMに置き、変数は書き換えられるようにSDRAMに置くといった具合です。

プログラムをロードする

このとき、配置の単位となるのがセクションです。セクションは、どのような特性を持ったメモリ(セクション属性)に、どんなデータ(初期値)を、どこから(アドレス[VMA/LMA])置くかといった情報を持っています。セクション情報はプログラムの意味を知っていないと作ることができないため、通常コンパイラが生成します。開発者が自由に作ることもできます。プログラムローダはこれらセクションの情報を読み、正しい場所にプログラムを格納していきます。ちなみに、ROMから起動するようなプログラムの場合、自分がプログラムローダの代わりにプログラムをROMに正しく配置する必要があります。このときにも、セクションが持っている情報がとっても重要で、人間がこの情報を元に正しい位置に配置しなければプログラムは正しく動かないのです。


セクションが持つ情報

セクションはさまざまな情報を持っています。ここではその情報について、ELFオブジェクトファイルフォーマットのセクションを例に挙げて説明します。


特に重要なセクション

セクションのなかには、固有の名前を持った特に重要なセクションが存在します。これらは主にコンパイラやアセンブラが生成するセクションで、コンパイラの規約やオブジェクトファイルフォーマットの規約によって定められた「固有の名前」を持っています。ここではそのいくつかを紹介します。

セクション情報は、GNUツールならobjdump -h ファイル名、Visual C++ならdumpbin /HEADERS ファイル名というコマンドで見ることができます。たとえば、Hello the world!のプログラムのセクション情報を見ると、次のようになっています (cygwin 1.3.5 + gcc 2.95.3 + binutils 2.11)。

Hello, the world! のセクション情報
$ objdump -h ./a.exe

./a.exe: file format pei-i386

Sections:
IdxNameSizeVMALMAFile offAlgn
0.text000004000040100000401000000004002**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
1.data000002000040200000402000000008002**2
CONTENTS, ALLOC, LOAD, DATA
2.bss0000000c0040300000403000000000002**2
ALLOC
3.idata00000200004040000040400000000a002**2
CONTENTS, ALLOC, LOAD, DATA
4.stab0000c400004050000040500000000c002**2
CONTENTS, READONLY, DEBUGGING, NEVER_LOAD, EXCLUDE
5 .stabstr0004c20000412000004120000000d0002**0
CONTENTS, READONLY, DEBUGGING, NEVER_LOAD, EXCLUDE

objdumpでは、SHT_PROGBITS -> CONTENTS, SHF_ALLOC -> ALLOC, SHF_WRITEでない -> READONLY, SHF_EXECINSTR -> CODEと表示されています。


コンパイラで遊ぼう - 属性

それでは簡単にセクションを使って遊んで見ましょう。

まずは次のような簡単なプログラムを作成します。

#include <stdio.h>

int global = 123;

int main(void)
{
static int static_local = 456;
int local = 789;

printf("%d,%d,%d\n", global, static_local, local);
return 0;
}

このプログラムをgccでコンパイルして実行すると、当然 "123,456,789"と表示されるはずです。しかし、ここでちょっとした細工を加えます。

% objcopy a.out --set-section-flags .data=alloc

これは何をしているかというと、本来.dataセクションは初期値をデータとして持っているのですが、その初期値をメモリに置かなくてもよいと指示しています(ただし変数を置く場所は必要なので、allocを指定してメモリ領域は確保させます)。すると、実行結果はこうなります(値は処理系などによって変わります)。

% ./a.out
0,0,789
%

このように、大域変数と静的局所変数の初期値が吹っ飛んでしまいました。ちなみに自動局所変数の初期値が吹っ飛んでいないのは、自動局所変数はメモリ上に置かれた値で初期化されるではなく、命令によって初期化されるからです(逆アセンブルするとよくわかります)。

#.rodataを入れ替えるとかもやろうと思ったけど、プログラムが吹っ飛ぶ可能性が高いので、このくらいでやめておきます。