データセクション
前回は、コードセクション (.text) でデータを定義しましたが、通常、データは、「.data」や「.rodata」などのセクションに出力します。
.data | 初期化されているデータ。 読み書き可能で、初期値が必要なグローバル変数として使う。 |
---|---|
.rodata .rdata | 読み込み専用データ。 (Unix/MacOS の場合は .rodata。PE の場合は .rdata) |
.bss | 初期化されていないデータ。 (実行時に確保され、0 で初期化される) 読み書き可能であるが、初期値が必要ない、または 0 で初期化するグローバル変数として使う。 |
サンプルコード (1)
.data と .rodata セクションを使ったサンプルを実行してみます。
今回も、C ソースの方は test1.c を使います。
<07a_data.asm>
今回も、C ソースの方は test1.c を使います。
<07a_data.asm>
default rel global testfunc ; .rodata section .rodata rdata: dd 100 ; .data section .data rwdata: dd 200 ; コード section .text testfunc: mov ecx, [rdata] ; ECX = 100 add ecx, [rwdata] ; ECX = 100+200 = 300 mov [rwdata], ecx ; [rwdata] = ECX mov eax, [rwdata] ; EAX = [rwdata] (300) ret
$ nasm -f elf64 07a_data.asm $ cc -o test test1.c 07a_data.o $ ./test 300
解説
default rel で、RIP 相対アドレスを使うことを指定します。
rdata のラベル位置に、読み込み専用データ (.rodata) として、32bit 値の 100 を定義。
rwdata のラベル位置に、読み書き可能データ (.data) として、32bit 値の 200 を定義します。
まず、ECX レジスタに、rdata ラベル位置の 100 の値を読み込み、その後、ADD 命令で、ECX に rwdata ラベル位置の 200 の値を加算します。
この時点で、ECX レジスタの値は 300 です。
その ECX レジスタの値を、一旦 rwdata のラベル位置に書き込みます (読み書きが可能であることを確認するため)。
その後、rwdata のラベル位置から、EAX に値を読み込みます。
結果として、300 の値が、戻り値として返ります。
ちなみに、読み込み専用である、rdata のラベル位置に値を書き込もうとすると、Segmentation fault になります。
rdata のラベル位置に、読み込み専用データ (.rodata) として、32bit 値の 100 を定義。
rwdata のラベル位置に、読み書き可能データ (.data) として、32bit 値の 200 を定義します。
まず、ECX レジスタに、rdata ラベル位置の 100 の値を読み込み、その後、ADD 命令で、ECX に rwdata ラベル位置の 200 の値を加算します。
この時点で、ECX レジスタの値は 300 です。
その ECX レジスタの値を、一旦 rwdata のラベル位置に書き込みます (読み書きが可能であることを確認するため)。
その後、rwdata のラベル位置から、EAX に値を読み込みます。
結果として、300 の値が、戻り値として返ります。
ちなみに、読み込み専用である、rdata のラベル位置に値を書き込もうとすると、Segmentation fault になります。
サンプルコード (2)
次は、.bss セクションを使ったサンプルです。
<07b_bss.asm>
<07b_bss.asm>
default rel global testfunc ; .bss section .bss data1: resd 2 data2: resd 1 ; コード section .text testfunc: mov dword [data1], 10 mov dword [data1+4], 20 mov eax, [data1] ; EAX = 10 add eax, [data1+4] ; EAX = 10+20 = 30 add eax, [data2] ; EAX = 30+0 = 30 ret
$ nasm -f elf64 07b_bss.asm $ cc -o test test1.c 07b_bss.o $ ./test 30
解説
.bss セクションは、セクション全体のサイズが情報として記録されるだけで、データとしては何も出力されません。
BSS セクションにデータ領域を定義する場合は、RESx 疑似命令を使います。
BSS セクションのデータは、プログラムの実行時に確保され、0 で初期化されます。
読み書き可能な領域なので、確保された後は、プログラム側で値を書き込んだり、読み込んだりすることができます。
上記のプログラムの場合、data1 の位置に 32bit x 2、data2 の位置に 32bit x 1 を確保しています。
testfunc 関数内では、data1 の位置に 10 の値を書き込み、data1 + 4 の位置に 20 の値を書き込んでいます。
(オフセット値はバイト単位で指定します)
MOV 命令で、メモリ位置に即値を代入する場合、mov [data1], 10 だけでは、実際にどのサイズの値を書き込むかが判断できないため、アドレス指定の前に dword (32bit) のキーワードを付けて、明示的にサイズを指定する必要があります。
data2 の位置には何も書き込んでいませんが、領域は確保時に 0 で初期化されているため、値は 0 です。
次に、EAX レジスタに data1 の位置の値を読み込み (10)、さらに、ADD 命令で、data1 + 4 の位置から 20 の値を読み込んで加算し (10 + 20 = 30)、その後、data2 の位置から 0 の値を読み込んで加算します (30 + 0 = 30)。
結果、EAX の値は 30 となるので、戻り値は 30 です。
BSS セクションにデータ領域を定義する場合は、RESx 疑似命令を使います。
BSS セクションのデータは、プログラムの実行時に確保され、0 で初期化されます。
読み書き可能な領域なので、確保された後は、プログラム側で値を書き込んだり、読み込んだりすることができます。
上記のプログラムの場合、data1 の位置に 32bit x 2、data2 の位置に 32bit x 1 を確保しています。
testfunc 関数内では、data1 の位置に 10 の値を書き込み、data1 + 4 の位置に 20 の値を書き込んでいます。
(オフセット値はバイト単位で指定します)
MOV 命令で、メモリ位置に即値を代入する場合、mov [data1], 10 だけでは、実際にどのサイズの値を書き込むかが判断できないため、アドレス指定の前に dword (32bit) のキーワードを付けて、明示的にサイズを指定する必要があります。
data2 の位置には何も書き込んでいませんが、領域は確保時に 0 で初期化されているため、値は 0 です。
data1: 10, 20 data2: 0
次に、EAX レジスタに data1 の位置の値を読み込み (10)、さらに、ADD 命令で、data1 + 4 の位置から 20 の値を読み込んで加算し (10 + 20 = 30)、その後、data2 の位置から 0 の値を読み込んで加算します (30 + 0 = 30)。
結果、EAX の値は 30 となるので、戻り値は 30 です。
GLOBAL/EXTERN
同じソースファイル内で定義されたデータであれば、普通に参照できますが、別のオブジェクトファイルで定義されたデータ (関数も同様) を参照したい場合は、GLOBAL と EXTERN を使って、シンボル名を宣言する必要があります。
GLOBAL は、自ソース内で定義されたシンボルを、他のオブジェクトから参照できるようにするために宣言します。
EXTERN は、他のオブジェクトで定義されたシンボルを、自ソース内で使用できるようにするために宣言します。
データを参照する側:
データを参照される側:
カンマ (,) で区切ることで、複数のシンボルを指定することもできます。
GLOBAL は、自ソース内で定義されたシンボルを、他のオブジェクトから参照できるようにするために宣言します。
EXTERN は、他のオブジェクトで定義されたシンボルを、自ソース内で使用できるようにするために宣言します。
データを参照する側:
extern gdata ; 他のオブジェクトで定義された gdata を参照できるように宣言 section .text testfunc: mov eax, [rel gdata] ret
データを参照される側:
global gdata ; gdata を他のオブジェクトから参照できるようにするため、宣言 section .data gdata: dd 12
カンマ (,) で区切ることで、複数のシンボルを指定することもできます。
追加の情報
GLOBAL や EXTERN でシンボルを宣言する際、シンボル名の後にコロン (:) を付けて、出力形式固有の単語を続けると、そのシンボルに対する、追加の情報を指定できます。
出力形式が ELF の場合は、シンボルが、関数またはデータのいずれであるかを明確に指定できます。
function の場合は関数、data または object の場合は、データのシンボルとして定義されます。
この情報は、ELF のシンボルテーブルに、シンボルタイプとしてセットされます。
デフォルトでは、タイプは「なし」になります。
実行ファイルの動作自体には影響しませんが、必要であれば指定しておくと良いでしょう。
07a_data.asm の「global testfunc」を「global testfunc:function」に置き換えてコンパイルすると、タイプが FUNC に変わります。
出力形式が ELF の場合は、シンボルが、関数またはデータのいずれであるかを明確に指定できます。
global symbol_func:function global symbol_data:data
function の場合は関数、data または object の場合は、データのシンボルとして定義されます。
この情報は、ELF のシンボルテーブルに、シンボルタイプとしてセットされます。
デフォルトでは、タイプは「なし」になります。
実行ファイルの動作自体には影響しませんが、必要であれば指定しておくと良いでしょう。
シンボルテーブル
例えば、readelf コマンドで、07a_data.o のシンボルテーブルを表示すると、rdata, rwdata, testfunc の各シンボルのタイプは、いずれも NOTYPE になっています。$ readelf -s 07a_data.o Symbol table '.symtab' contains 8 entries: 番号: 値 サイズ タイプ Bind Vis 索引名 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 FILE LOCAL DEFAULT ABS ... 2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 .rodata 3: 0000000000000000 0 SECTION LOCAL DEFAULT 2 .data 4: 0000000000000000 0 SECTION LOCAL DEFAULT 3 .text 5: 0000000000000000 0 NOTYPE LOCAL DEFAULT 1 rdata 6: 0000000000000000 0 NOTYPE LOCAL DEFAULT 2 rwdata 7: 0000000000000000 0 NOTYPE GLOBAL DEFAULT 3 testfunc
07a_data.asm の「global testfunc」を「global testfunc:function」に置き換えてコンパイルすると、タイプが FUNC に変わります。
7: 0000000000000000 0 FUNC GLOBAL DEFAULT 3 testfunc