メモリは通常、オペレーティングシステム(OS)によって、プログラムのために次のような一般的なレイアウトで割り当てられます。
| アドレス | メモリ領域 |
|---|---|
| 高位 | スタック |
| ... | |
| ヒープ | |
| 読み書き可能セグメント | |
| コード・読み取り専用セグメント | |
| 低位 | 予約済み |
セグメントに分けられたメモリは、それぞれ異なる権限を持つセクションに整理されています。
これまで定義してきた関数は、すべてSection .textにありました。 このセクションには、読み取り専用の実行可能なデータが格納されます。 ほかのセクションはデータ変数を宣言するために使われます。データ変数は読み取り専用の場合も読み書き可能な場合もありますが、実行可能ではありません。
初期化されたデータは、Section .dataで宣言します。
NASM(The Netwide Assembler。このトラックで使うアセンブラーです)では、初期化された変数は、名前、データサイズを示すディレクティブ、カンマで区切られた値のリストからなります。
それぞれはスペースで区切られ、ラベルの後ろには必要に応じて:を付けられます。
主なディレクティブと、それに対応するデータサイズは次のとおりです。
| ディレクティブ | サイズ |
|---|---|
| db | 1バイト |
| dw | 2バイト |
| dd | 4バイト |
| dq | 8バイト |
たとえば、次のコードはspaceという名前のバイト変数を値10で宣言します。
section .data
space db 10
section .dataで宣言した変数は可変、つまり読み書き可能です。
また、_静的_な記憶域期間を持ちます。これは、プログラムの実行中ずっと存在することを意味します。
Section .rodataはsection .dataと似ています。
どちらのセクションも初期化されたデータを含み、同じ方法で宣言され、同じ記憶域期間を持ちます。
主な違いは、section .rodataのデータが不変、つまり読み取り専用であることです。
equで定義した定数は、section .rodataで定義した定数とは異なります。
equで定義した定数はメモリ上に領域を占めず、アセンブラーによってその値に直接置き換えられます。
実際には、その値のためのプレースホルダーなのです。
一方、section .rodataで定義した定数は実際にメモリに格納され、アドレスを持ちます。
宣言したデータには、名前を関連付ける必要があります。 この名前をラベルと呼びます。
ラベルは、メモリ内のデータの特定のアドレスを表すシンボルです。 x86-64のアドレスは64ビットの値です。
NASMでは、ラベルを使ってデータに直接アクセスしようとしても、割り当てられたメモリそのものではなく、そのアドレスが得られます。
section .data
example dq 27 ; this declares a 8-byte variable initialized with 27
section .text
fn:
mov rax, example ; this stores the address of the declared variable in rax, not its contents
...
メモリアドレスの内容にアクセスするには、そのアドレスを_逆参照_する必要があります。 これを間接参照と呼びます。
NASMでは、[]を使って行います。
section .data
example dq -27 ; this declares a 8-byte variable initialized with -27
section .text
fn:
mov rax, [example] ; this dereferences example and access the value stored in memory (-27)
...
ただし、逆参照するメモリのサイズが曖昧になる状況もあります。 そのような場合は、サイズを指定するプレフィックスを使う必要があります。
一般的なx86-64プログラムで重要なプレフィックスとそのサイズは次のとおりです。
| プレフィックス | サイズ |
|---|---|
| byte | 1バイト |
| word | 2バイト |
| dword | 4バイト |
| qword | 8バイト |
同じロードは、サイズを明示して書くこともできます。
mov rax, qword [example] ; same dereference, size stated explicitly
メモリを逆参照するときは、常にプレフィックスを付けるのがよい習慣です。
メモリへの書き込みも同じ方法で、アドレスを逆参照して行います。
section .data
example1 db 10 ; example1 is a 1-byte memory location initialized with value 10
example2 dq -456 ; example2 is a 8-byte memory location initialized with value -456
example3 dd 54 ; example3 is a 4-byte memory location initialized with value 54
section .text
fn:
mov byte [example1], 20 ; example1 now has value 20
mov qword [example2], rdx ; example2 now has value equal to the contents in rdx
mov dword [example3], eax ; example3 now has value equal to the contents in eax
ほとんどの命令では、内容をレジスターに読み込まなくてもメモリオペランドを使えることに注意してください。 ただし、通常はソースとデスティネーションの両方のオペランドには使えず、どちらか一方だけに使えます。
section .data
example4 dw 4
example5 dq -8
example6 dd 15
section .text
fn:
add word [example4], 5 ; example4 is now a 2-byte memory location with the value 4 + 5 = 9
imul rax, qword [example5] ; rax = rax * (-8)
; this is not possible -> sub dword [example6], dword [example6]
movを使って変数のアドレスをレジスターに格納することもできますが、この目的専用の命令があります。それがleaです。
この命令はメモリ形式のオペランドを使いますが、メモリを読み取ることは_ありません_。 代わりに、実効アドレスの式を計算し、その結果をデスティネーションオペランドに書き込みます。
lea rax, [example] ; this stores the address of 'example' in rax
メモリアドレスを計算してレジスターに格納するには、leaを使うほうがより自然です。
メモリ位置にアクセスするとき、NASMのデフォルトの動作は絶対アドレス、つまり固定されたメモリアドレスを生成することです。
セキュリティ上の理由から、実行ファイルはしばしば**PIE(位置独立実行ファイル)**としてビルドされ、メモリ領域はランダムな位置に配置されます。
PIEでは、変数の最終的なアドレスはリンク時にわかりません。
そのため、コードはripという特別なレジスターの値からのオフセットとしてアドレスを計算します。ripは次に実行される命令を指します。
これは通常、RIP相対アドレス指定と呼ばれます。
NASMでは、rel演算子を使ってRIP相対アクセスを指定できます。
mov rax, qword [rel variable]
ファイルの先頭にdefault relと書けば、そのソースファイル全体で相対アドレス指定をデフォルトにできます。
このトラックのすべての演習はPIEとしてコンパイル・リンクされるので、相対アドレスを生成するにはrelを使うようにします。
どのセクション(たとえば.text、.data、.rodata)で定義したラベル(関数やデータ)も、同じソースファイル内から見えます。
globalで宣言すれば、ほかのソースファイルからも見えます。
逆に、ほかのソースファイルで定義したラベルは、externで宣言すれば現在のソースファイルから見えます。
この場合、アセンブリーにはデータサイズの記載がないため、あらかじめ把握しておく必要があります。
default rel
section .data
global number1 ; 'number1' is a variable visible to other source files
number1 db 200
extern number2 ; 'number2' is a variable visible to the current source file, but defined in another
section .text
extern sum ; sum is a function visible to the current source file, but defined in another
fn:
mov dil, byte [number1]
mov sil, byte [number2]
call sum
...
友人のJoséは、地元の学校の先生です。 色を組み合わせて別の色を作る方法を、楽しい実験で見せられると思いつきました。
そこで、その実験について助けを求めてきました。
この演習では、色は32ビット(4バイト)の数値として表され、そのRGB値がエンコードされています。
RGB値は、Red、Green、_Blue_の3つのチャンネルからなり、それぞれが8ビット(1バイト)を占めます。
4バイト目は通常_Alpha_チャンネル用に確保されますが、この演習ではその値は空(0)になります。
それぞれの色の値は、別のソースファイルで定義されたテーブルにすでに格納されています。 色は、このテーブル内の一意なアドレスで識別されます。
色の32ビット値を返す関数get_color_valueを定義します。
この関数は、色テーブル内にあるこの色の有効なアドレスを仮引数として受け取ります。
get_color_value(black)
// => 0
ヒント - 32ビットは4バイトに相当します。
いろいろな色を混ぜるために、Joséはまず基本となる色を1つ決め、それと組み合わせる二次色だけを変えます。
色の32ビット値を変数base_colorに保存し、あとで使えるようにする関数add_base_colorを定義します。
この関数に戻り値はなく、色テーブル内にあるその色のアドレスを仮引数として受け取ります。
変数base_colorは自分で定義し、ほかのソースファイルからも参照できるようにする必要があります。
同時に存在する基本色は1つだけです。 新しい基本色が追加されると、古いものは破棄されます。
既定では、プログラムの開始時に、base_colorを_white_の32ビット値である0xFFFFFF00で初期化します。
ヒント - NASMは、0xFFFFFF00のように、先頭に0xを付けた16進数で表された数値を受け付けます。
Joséは原色を使った組み合わせをたくさん作る予定なので、すばやく使えるようにそれらを分けておきたいと考えています。
色の表現にRGBを使うため、原色は次の3つです。
RED、値は0xFF000000。GREEN、値は0x00FF0000。BLUE、値は0x0000FF00。これらの色それぞれについて、定数を1つずつ定義します。 これらの定数は、ほかのソースファイルからも参照できるようにする必要があります。
色の組み合わせ方は、別のソースファイルで定義されたcombining_functionに従います。
この関数は、仮引数としてbase_colorと、それと混ぜる二次色の32ビット値を受け取ります。
そして、組み合わせた色の32ビット値を返します。
2つの色を組み合わせ、その結果をメモリに保存する関数make_color_combinationを定義します。
この関数に戻り値はなく、仮引数として次の順番で受け取ります。
combining_functionは、使用中のレジスタの値を書き換える可能性があることに注意してください。
関数を呼び出す前に、必要な変数は必ずメモリに保存しておいてください。
Exercismに登録すれば、22個のコンセプト130個の演習、そして本物の人間によるメンタリングとともに、x86-64 Assemblyを学んでマスターできます。すべて無料です。