コンピュータの心臓部に触れる第一歩 💻
こんにちは!このブログ記事では、コンピュータが直接理解する言葉に近い「アセンブリ言語」の世界、特に広く使われてきた「x86アーキテクチャ」の32ビット版について、基本から解説していきます。
現代のプログラミングでは、PythonやJava、C++といった「高級言語」を使うことがほとんどです。これらの言語は人間にとって理解しやすく、複雑な処理も比較的簡単に記述できます。しかし、コンピュータが実際に実行しているのは、これらの高級言語が「コンパイラ」や「インタプリタ」によって翻訳された、「機械語」と呼ばれる単純な命令の集まりです。
アセンブリ言語は、この機械語とほぼ1対1に対応する低級言語です。これを学ぶことで、以下のようなメリットがあります。
- コンピュータが内部でどのように動作しているかを深く理解できる 🤔
- プログラムのパフォーマンスを極限まで追求できる 🚀
- OSやデバイスドライバなど、よりハードウェアに近い領域の開発に関われる
- 既存のプログラムがどのように動作しているかを解析(リバースエンジニアリング)するスキルが身につく 🔍
現在主流のCPUは64ビットですが、32ビットのx86アセンブリ(IA-32とも呼ばれます)を学ぶことには、依然として価値があります。アーキテクチャが比較的シンプルで学習しやすく、多くのOSや組み込みシステム、古いソフトウェアなど、今でも様々な場面で使われています。また、64ビットアーキテクチャを学ぶ上での基礎固めにもなります。
この入門記事を通して、アセンブリ言語の基本的な概念、ツールの使い方、簡単なプログラムの作成方法を学び、コンピュータの仕組みへの理解を深めていきましょう!
🛠️ 開発環境の準備
アセンブリ言語でプログラミングを始めるには、いくつかツールが必要です。ここでは、Linux環境(特にDebianベースのUbuntuなど)でよく使われるツールを紹介します。WindowsやmacOSでも同様のツールがありますが、設定方法が異なる場合があります。
アセンブラ (Assembler)
アセンブリ言語のコード(ソースコード)を、コンピュータが実行できる機械語(オブジェクトコード)に変換するプログラムです。代表的なものに以下の2つがあります。
- NASM (Netwide Assembler): シンプルな構文で人気のあるアセンブラです。Intel記法に近い構文を採用しています。多くのプラットフォームで利用可能です。今回は主にNASMを使います。
- GAS (GNU Assembler): GCC(GNU Compiler Collection)に含まれているアセンブラです。AT&T記法という、NASMとは少し異なる構文を採用しています。Linux環境では標準的に使われています。
Ubuntu/Debian系LinuxでのNASMのインストールは、ターミナルで以下のコマンドを実行します。
sudo apt-get update
sudo apt-get install nasm
リンカ (Linker)
アセンブラが生成したオブジェクトコードや、他のライブラリファイルなどを結合して、実行可能なプログラムを作成するツールです。Linuxでは通常、ld
(GNU Linker) が使われます。GCCを介して自動的に呼び出されることも多いです。
GCC (build-essential) をインストールすれば、通常ldも含まれています。
sudo apt-get install build-essential
デバッガ (Debugger)
プログラムの実行をステップごとに追いかけたり、レジスタやメモリの内容を確認したりして、バグの原因特定を助けるツールです。LinuxではGDB
(GNU Debugger) が広く使われています。
GDBもbuild-essentialに含まれていることが多いですが、もしなければ個別にインストールします。
sudo apt-get install gdb
🧠 基本的な概念
アセンブリプログラミングを理解するために、いくつかの重要な概念を学びましょう。
レジスタ (Registers)
レジスタは、CPU内部にある高速な記憶領域です。計算を行うデータや、メモリのアドレスなどを一時的に保持するために使われます。32bit x86アーキテクチャには、主に以下の汎用レジスタがあります。
レジスタ名 (32bit) | 下位16bit | 上位8bit (of 16bit) | 下位8bit (of 16bit) | 主な用途 (慣例的なものも含む) |
---|---|---|---|---|
EAX | AX | AH | AL | アキュムレータ (演算結果、関数の戻り値) |
EBX | BX | BH | BL | ベースレジスタ (データ領域へのポインタ) |
ECX | CX | CH | CL | カウンタレジスタ (ループカウンタ) |
EDX | DX | DH | DL | データレジスタ (演算データ、I/Oポートアドレス) |
ESI | SI | – | ソースインデックス (文字列操作のソースポインタ) | |
EDI | DI | – | デスティネーションインデックス (文字列操作のデスティネーションポインタ) | |
EBP | BP | – | ベースポインタ (スタックフレームの基点) | |
ESP | SP | – | スタックポインタ (スタックの現在の頂点) |
この他に、プログラムの実行位置を示すEIP
(インストラクションポインタ) や、演算結果の状態を示すEFLAGS
(フラグレジスタ) などがあります。
メモリとアドレッシング (Memory and Addressing)
プログラムやデータは、メインメモリ(RAM)上に格納されます。CPUは、メモリアドレスを指定して、メモリ上のデータにアクセスします。32bitアーキテクチャでは、アドレスは32ビットで表現され、最大で 232 バイト (4GB) のメモリ空間を扱うことができます。
アセンブリでは、様々な方法でメモリアドレスを指定できます(アドレッシングモード)。
- 直接アドレッシング:
mov eax, [0x12345678]
(特定のアドレスの内容をEAXにコピー) - レジスタ間接アドレッシング:
mov eax, [ebx]
(EBXレジスタが指すアドレスの内容をEAXにコピー) - ディスプレースメント付きアドレッシング:
mov eax, [ebx + 4]
(EBXの値+4のアドレスの内容をEAXにコピー) - インデックス付きアドレッシング:
mov eax, [ebx + esi * 4]
(EBX + ESI*4 のアドレスの内容をEAXにコピー)
角括弧 []
は、その中身がメモリアドレスであることを示します。
命令 (Instructions)
CPUが実行する操作の単位です。各命令は「オペコード」と、操作対象を示す「オペランド」から構成されます。基本的な命令をいくつか紹介します。
MOV destination, source
: sourceからdestinationへデータをコピー(移動)します。例:mov eax, 10
(EAXに即値10をコピー),mov ebx, eax
(EAXの内容をEBXにコピー)ADD destination, source
: destinationにsourceを加算します。例:add eax, 5
(EAXに5を加算)SUB destination, source
: destinationからsourceを減算します。例:sub ebx, ecx
(EBXからECXを減算)INC destination
: destinationの値を1増やします。例:inc ecx
DEC destination
: destinationの値を1減らします。例:dec edx
PUSH value
: valueをスタックに積みます。スタックポインタ(ESP)がデクリメントされます。例:push eax
POP destination
: スタックから値を取り出してdestinationに格納します。スタックポインタ(ESP)がインクリメントされます。例:pop ebx
CALL address/label
: サブルーチン(関数)を呼び出します。戻りアドレスをスタックに積み、指定されたアドレスにジャンプします。例:call my_function
RET
: サブルーチンから戻ります。スタックから戻りアドレスを取り出して、そこにジャンプします。JMP address/label
: 指定されたアドレスに無条件にジャンプします。例:jmp loop_start
CMP operand1, operand2
: operand1とoperand2を比較します。結果はフラグレジスタ(EFLAGS)に反映されます。- 条件付きジャンプ命令:
CMP
命令の結果に基づいてジャンプします。JE label
(Jump if Equal): 等しい場合にジャンプJNE label
(Jump if Not Equal): 等しくない場合にジャンプJG label
(Jump if Greater): operand1 > operand2 の場合にジャンプ (符号付き)JL label
(Jump if Less): operand1 < operand2 の場合にジャンプ (符号付き)JGE label
(Jump if Greater or Equal): operand1 >= operand2 の場合にジャンプ (符号付き)JLE label
(Jump if Less or Equal): operand1 <= operand2 の場合にジャンプ (符号付き)JA label
(Jump if Above): operand1 > operand2 の場合にジャンプ (符号なし)JB label
(Jump if Below): operand1 < operand2 の場合にジャンプ (符号なし)- …他にも多数あります。
INT value
: ソフトウェア割り込みを発生させます。OSの機能を呼び出す(システムコール)際によく使われます。Linux 32bitではint 0x80
が一般的です。
データ型 (Data Types)
アセンブリでは、扱うデータのサイズを明示的に意識する必要があります。NASMでは、データ定義ディレクティブを使ってメモリ上にデータを確保します。
DB
(Define Byte): 1バイト (8ビット) のデータを定義します。DW
(Define Word): 2バイト (16ビット) のデータを定義します。DD
(Define Double Word): 4バイト (32ビット) のデータを定義します。DQ
(Define Quad Word): 8バイト (64ビット) のデータを定義します。RESB
,RESW
,RESD
,RESQ
: 指定したサイズの未初期化領域を確保します。TIMES
: 命令やデータを繰り返します。例:times 100 dd 0
(0で初期化されたDWORDを100個確保)
例:
section .data
myByte db 10 ; 1バイトの値10
myWord dw 1000 ; 2バイトの値1000
myDword dd 12345678 ; 4バイトの値12345678
myString db 'Hello!', 0 ; 文字列 (C言語形式のNULL終端)
myBuffer resb 256 ; 256バイトの未初期化領域
システムコール (System Calls)
アプリケーションプログラムがOSの機能(ファイル操作、画面出力、プロセス終了など)を利用するための仕組みです。Linux 32bit環境では、int 0x80
割り込みを使ってシステムコールを呼び出すのが伝統的な方法です。
呼び出すには、通常以下の手順を踏みます。
EAX
レジスタに、呼び出したいシステムコールの番号を設定します。- 引数を順番に
EBX
,ECX
,EDX
,ESI
,EDI
,EBP
レジスタに設定します。 int 0x80
命令を実行します。- 戻り値が
EAX
レジスタに格納されます(エラーの場合は負の値など)。
システムコール番号はOSやアーキテクチャによって異なります。Linux 32bitの主なシステムコール番号の例:
1
:sys_exit
(プログラム終了)3
:sys_read
(読み込み)4
:sys_write
(書き込み)5
:sys_open
(ファイルを開く)6
:sys_close
(ファイルを閉じる)
(システムコール番号の一覧は、通常 `/usr/include/asm/unistd_32.h` などで確認できますが、環境により異なります。)
👋 Hello, World! プログラム
それでは、実際に簡単なプログラムを作成してみましょう。画面に “Hello, World!” と表示して終了するプログラムです。
以下のコードを hello.asm
という名前で保存してください。
section .data
; データセクション: 初期化されたデータを定義する場所
hello_msg db 'Hello, World!', 0x0A ; 表示するメッセージ (0x0A は改行コード)
msg_len equ $ - hello_msg ; メッセージの長さを計算 (equは定数を定義)
section .text
; テキストセクション: 実際のプログラムコードを置く場所
global _start ; リンカに対して _start ラベルを公開する (プログラムのエントリーポイント)
_start:
; --- メッセージを画面 (標準出力) に表示するシステムコール (sys_write) ---
mov eax, 4 ; システムコール番号 4 (sys_write) を EAX に設定
mov ebx, 1 ; ファイルディスクリプタ 1 (標準出力 stdout) を EBX に設定
mov ecx, hello_msg ; 表示する文字列のアドレスを ECX に設定
mov edx, msg_len ; 文字列の長さを EDX に設定
int 0x80 ; システムコールを実行!
; --- プログラムを正常終了させるシステムコール (sys_exit) ---
mov eax, 1 ; システムコール番号 1 (sys_exit) を EAX に設定
xor ebx, ebx ; 終了コード 0 を EBX に設定 (xor ebx, ebx は mov ebx, 0 と同じ効果で、より短い命令)
int 0x80 ; システムコールを実行!
コード解説
section .data
/section .text
: プログラムを論理的なセクションに分割します。.data
には初期値を持つ変数や定数、.text
には実行されるコードを記述します。hello_msg db 'Hello, World!', 0x0A
:hello_msg
というラベル名で、文字列 “Hello, World!” と改行コード (0x0A
) をメモリ上に確保します。db
は Define Byte の略です。msg_len equ $ - hello_msg
:$
は現在のアドレスを示します。$ - hello_msg
で、hello_msg
の開始アドレスから現在のアドレスまでのバイト数を計算し、msg_len
という定数に割り当てます。これでメッセージの長さを自動計算できます。equ
は定数を定義するディレクティブです。global _start
:_start
ラベルをプログラムの外部(具体的にはリンカ)から見えるように宣言します。Linuxでは、プログラムの実行開始点を_start
ラベルとするのが慣例です。_start:
: プログラムの実行が開始される場所を示すラベルです。mov eax, 4
…int 0x80
:sys_write
システムコールを呼び出して画面に文字列を表示します。各レジスタにシステムコール番号と引数を設定しています。mov eax, 1
…int 0x80
:sys_exit
システムコールを呼び出してプログラムを終了します。xor ebx, ebx
は効率的に EBX レジスタを0にするためのテクニックです。
アセンブルとリンク、実行
ターミナルで以下のコマンドを実行します。
-
アセンブル (NASM): アセンブリコード (
.asm
) をオブジェクトファイル (.o
) に変換します。nasm -f elf hello.asm -o hello.o
-f elf
: 出力フォーマットとしてELF (Executable and Linkable Format) を指定します。これはLinuxの標準的なフォーマットです (32bitなのでelf
、64bitならelf64
)。-o hello.o
: 出力ファイル名をhello.o
に指定します。
-
リンク (ld): オブジェクトファイルをリンクして、実行可能なファイルを作成します。
ld -m elf_i386 hello.o -o hello
-m elf_i386
: 32bit ELF (i386アーキテクチャ) としてリンクすることを指定します。-o hello
: 出力する実行可能ファイル名をhello
に指定します。
-
実行: 作成されたプログラムを実行します。
./hello
実行すると、ターミナルに以下のように表示されるはずです。
Hello, World!
🔄 基本的な制御フロー
プログラムの流れを制御するための基本的な方法、つまり条件分岐とループを見ていきましょう。これらは CMP
命令と条件付きジャンプ命令 (JE
, JNE
, JG
, JL
など) を組み合わせて実現します。
条件分岐 (If文のようなもの)
特定の値 (例えば EAX の値) が 10 であれば特定の処理を行う、というような条件分岐を実装してみます。
section .text
global _start
_start:
mov eax, 5 ; EAXに比較する値を入れる (ここでは5)
cmp eax, 10 ; EAXと10を比較
jne not_equal ; もし等しくなければ (Not Equal)、not_equal ラベルへジャンプ
; EAXが10の場合の処理 (ここにコードを書く)
; 例: mov ebx, 1 (何らかのフラグを立てるなど)
jmp end_if ; ifブロックの終わりにジャンプ
not_equal:
; EAXが10でない場合の処理 (ここにコードを書く)
; 例: mov ebx, 0
end_if:
; ... この後の処理 ...
; プログラム終了処理
mov eax, 1
xor ebx, ebx
int 0x80
CMP
命令で比較を行い、その結果に応じて JNE
(Jump if Not Equal) 命令で処理を分岐させています。目的の処理が終わったら、JMP
で条件分岐ブロックの終わり (end_if
) にジャンプし、処理が混ざらないようにします。
ループ (Loop)
特定の処理を繰り返すループも、ジャンプ命令を使って実装できます。ここでは、ECX レジスタをカウンタとして使い、5回処理を繰り返す例を示します。
section .text
global _start
_start:
mov ecx, 5 ; ループカウンタを5に初期化
loop_start:
; --- ループ内で行いたい処理 ---
; 例: ここで何かをする (画面表示、計算など)
; nop ; No Operation (何もしない命令、プレースホルダーとして)
; --- ここまで ---
dec ecx ; カウンタを1減らす
cmp ecx, 0 ; カウンタが0になったか比較
jne loop_start ; 0でなければ (Not Equal)、loop_start に戻ってループ継続
; ループ終了後の処理...
; プログラム終了処理
mov eax, 1
xor ebx, ebx
int 0x80
loop_start
ラベルがループの開始点です。ループ内の処理を実行した後、DEC
でカウンタを減らし、CMP
で0と比較します。JNE
命令で、カウンタが0でなければ loop_start
に戻り、ループを続けます。カウンタが0になるとジャンプせず、ループが終了します。
📞 関数 (プロシージャ) の作成と呼び出し
プログラムが大きくなってくると、処理を意味のある単位でまとめ、再利用したくなります。アセンブリ言語では、これを「プロシージャ」や「サブルーチン」と呼びます(ここでは「関数」と呼びます)。関数は CALL
命令で呼び出し、RET
命令で呼び出し元に戻ります。
関数呼び出しにおいて重要な役割を果たすのが「スタック」です。
CALL
命令:- 呼び出し元に戻るためのアドレス(
CALL
命令の次の命令のアドレス)をスタックに積む (PUSH)。 - 指定された関数のアドレスへジャンプする。
- 呼び出し元に戻るためのアドレス(
RET
命令:- スタックから戻りアドレスを取り出す (POP)。
- そのアドレスへジャンプする。
また、関数内でレジスタを使用する場合、呼び出し元で使用していたレジスタの値を壊さないように注意が必要です。一般的には、関数内で使用するレジスタの値を関数の最初にスタックに退避 (PUSH) し、関数の最後にスタックから復元 (POP) します。どのレジスタを保存すべきかは、コーディング規約(Calling Convention)によって定められていることが多いです。
引数やローカル変数の管理には、EBP
(ベースポインタ) と ESP
(スタックポインタ) を使うのが一般的です。
簡単な関数例
簡単な関数 print_message
を作成し、_start
から呼び出す例を見てみましょう。この関数は、ECX に格納されたアドレスの文字列を EDX に格納された長さだけ表示します。
section .data
msg1 db 'First message!', 0x0A
len1 equ $ - msg1
msg2 db 'Second message!', 0x0A
len2 equ $ - msg2
section .text
global _start
print_message:
; --- 関数 print_message: ECXのアドレスからEDXバイト表示 ---
; この関数は eax, ebx を使用するため、念のため保存する
push eax ; EAXをスタックに退避
push ebx ; EBXをスタックに退避
mov eax, 4 ; sys_write システムコール
mov ebx, 1 ; 標準出力
; ECX と EDX は呼び出し元で設定されている前提
int 0x80
pop ebx ; EBXをスタックから復元
pop eax ; EAXをスタックから復元
ret ; 呼び出し元に戻る
_start:
; 最初のメッセージを表示
mov ecx, msg1 ; 表示する文字列のアドレス
mov edx, len1 ; 文字列の長さ
call print_message ; 関数呼び出し
; 2番目のメッセージを表示
mov ecx, msg2
mov edx, len2
call print_message
; プログラム終了
mov eax, 1
xor ebx, ebx
int 0x80
この例では、print_message
関数内で EAX
と EBX
を使うため、関数の最初で PUSH
し、最後で POP
して元の値を復元しています。引数 (ECX, EDX) は呼び出し元で設定しています。CALL
で関数が呼び出され、処理が終わり RET
が実行されると、CALL
の次の行から実行が再開されます。
🐛 デバッグ入門 with GDB
アセンブリプログラミングでは、些細なミスが予期せぬ動作を引き起こすことがよくあります。そんな時に頼りになるのがデバッガです。ここでは、GDB (GNU Debugger) の基本的な使い方を紹介します。
GDBを使うためには、アセンブル・リンク時にデバッグ情報を含める必要があります。
nasm -f elf -g hello.asm -o hello.o # -g オプションを追加
ld -m elf_i386 hello.o -o hello
-g
オプションを NASM に付けることで、デバッグ情報がオブジェクトファイルに含まれます。
GDBを起動するには、ターミナルで以下のように実行します。
gdb ./hello
GDBのプロンプト (gdb)
が表示されたら、以下のコマンドがよく使われます。
break label
またはb label
: 指定したラベル (例:_start
,loop_start
) にブレークポイントを設定します。プログラム実行がその場所に到達すると一時停止します。run
またはr
: プログラムの実行を開始します。ブレークポイントに到達するか、プログラムが終了するまで実行されます。next
またはn
: 現在の行を実行し、次の行で停止します。関数呼び出しがあった場合、関数の中には入らずに関数全体を実行します。step
またはs
: 現在の行を実行し、次の命令で停止します。関数呼び出しがあった場合、その関数の中に入っていきます。info registers
またはi r
: すべての汎用レジスタの内容を表示します。特定のレジスタだけ見たい場合はi r eax ebx
のように指定します。x/format address
: 指定したアドレスのメモリの内容を表示します。format
で表示形式を指定できます。x/s address
: NULL終端文字列として表示x/i address
: 命令として逆アセンブルして表示 (例:x/i $eip
で現在の命令を表示)x/wx address
: 4バイトの16進数 (Word, heX) として表示x/10bx address
: 1バイトの16進数 (Byte, heX) を10個表示
print expression
またはp expression
: 式を評価して表示します。レジスタの値 (p $eax
) やラベルのアドレス (p hello_msg
) などを確認できます。continue
またはc
: プログラムの実行を再開します。次のブレークポイントまで実行されます。quit
またはq
: GDBを終了します。
🤔 なぜ今、32bit アセンブリを学ぶのか?
「最近のコンピュータはほとんど64bitなのに、なぜわざわざ32bitのアセンブリを学ぶ必要があるの?」と思うかもしれません。確かに、新しいソフトウェア開発の多くは64bit環境で行われています。しかし、32bit (IA-32) アセンブリを学ぶことには、依然としていくつかのメリットがあります。
- 教育的な価値: 32bitアーキテクチャは、64bitに比べてレジスタの数やアドレッシングモードが少なく、全体的にシンプルです。そのため、アセンブリ言語やコンピュータアーキテクチャの基本的な概念を学ぶ最初のステップとして適しています。基礎を固めてから64bitへ進むことで、よりスムーズに理解を深めることができます。
- 既存システムとの互換性: 世の中には、まだ多くの32bitシステムやソフトウェアが稼働しています。組み込みシステム、古い産業用制御システム、あるいはレガシーなOS環境などで動作するソフトウェアのメンテナンスや解析を行う場合、32bitアセンブリの知識が不可欠になることがあります。
- マルウェア解析やリバースエンジニアリング: 悪意のあるソフトウェア(マルウェア)の中には、解析を困難にするために古い技術や32bitコードを利用しているものも存在します。また、ソフトウェアの動作を理解するためにバイナリコードを解析するリバースエンジニアリングにおいても、32bitの知識が役立つ場面は少なくありません。
- 64bitへの橋渡し: 64bit (x86-64) アーキテクチャは、32bitアーキテクチャの拡張として設計されました。基本的な命令の多くは共通しており、レジスタが拡張され(RAX, RBX…)、アドレッシングモードが追加されています。32bitの知識があれば、これらの違いを理解することで、比較的容易に64bitアセンブリへ移行できます。
もちろん、最終的な目標が最新のシステム開発であれば、64bitアセンブリの学習も重要です。しかし、基礎をしっかりと学び、コンピュータの動作原理を深く理解するという点において、32bitアセンブリから始めることは有効なアプローチと言えるでしょう。
🚀 次のステップと参考情報
この入門記事で、x86 (32bit) アセンブリの基本的な世界に触れることができました。もし、さらに深く学びたいと思ったら、以下のステップに進むことをお勧めします。
- より複雑なプログラムを作成する: 条件分岐やループ、関数呼び出しを組み合わせて、もう少し複雑なアルゴリズム(例:簡単な計算、配列操作、文字列処理など)を実装してみましょう。
- C言語との連携: アセンブリで書いた関数をC言語から呼び出したり、逆にC言語の関数をアセンブリから呼び出したりする方法を学びます。これにより、パフォーマンスが重要な部分だけをアセンブリで記述するといったことが可能になります。(Calling Conventionの理解が重要になります。)
- デバッグスキルを向上させる: GDBのより高度な機能(ウォッチポイント、条件付きブレークポイント、メモリダンプなど)を使いこなせるようになりましょう。
- 64bit (x86-64) アセンブリへ: 32bitの知識をベースに、64bitアーキテクチャの違い(新しいレジスタ、アドレッシングモード、システムコールの呼び出し規約など)を学びます。
- 他のアーキテクチャ: ARMなど、x86以外のアーキテクチャのアセンブリ言語にも挑戦してみるのも面白いでしょう。特に組み込みシステムではARMが広く使われています。
- リバースエンジニアリング: 既存のプログラムのバイナリコードを読み解き、その動作を解析する技術を学びます。デバッガや逆アセンブラといったツールを駆使します。
参考情報
さらに学習を進めるための資料やウェブサイトをいくつか紹介します。
- NASM (The Netwide Assembler) 公式ドキュメント: NASMの詳細なマニュアルです。すべての命令やディレクティブについて解説されています。 https://www.nasm.us/docs.php
- Intel® 64 and IA-32 Architectures Software Developer Manuals: x86アーキテクチャに関する最も詳細で公式な情報源です。非常にボリュームがありますが、正確な情報を得るためには不可欠です。 https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html
- Assembly Language Step-by-Step: Programming with Linux (Jeff Duntemann): Linux環境でのNASMを使ったアセンブリプログラミングを丁寧に解説している書籍(英語)です。初心者向けに書かれています。
- x86 Assembly Guide (University of Virginia): 32bit x86アセンブリの基本をまとめたガイド(英語)です。 https://www.cs.virginia.edu/~evans/cs216/guides/x86.html (2022年3月8日時点の情報)
コメント