Twitterに書ききれないこと

イベントや技術的なことを記したい・・・

Practical Malware Analysis Chapter 6

マルウェア解析についての有名な書籍にPractical Malware Analysisがある。

その書籍を勉強のために翻訳・要約した記事。

全訳ではないので、詳細は原書を読んでください。

Practical Malware Analysis: The Hands-On Guide to Dissecting Malicious Software

Practical Malware Analysis: The Hands-On Guide to Dissecting Malicious Software

6章 アセンブリ内のCコード構文の認識

リバースエンジニアリングを行うとき、各命令を読むことはほとんどない。 マルウェア解析をするには、アセンブリ命令からC言語レベルのコードを読み取る必要がある。 このスキルの開発には、時間がかかる。

マルウェアC言語で作成されることが多い。 コードは、ループ、リンクドリスト、スイッチ文などを含む。 プログラムは個々の構成要素に分解することができる。 各構成要素が組み合わされて、プログラム全体の機能を実装している。

以下では10種類のC言語の構造について述べる。 Cはアセンブリに密接な関係を持つシンプルな言語えある。 すべての単一の命令を解析する必要はなく、プログラムの全体的な機能を理解することが重要である。

グローバル変数とローカル変数

グローバル変数は、プログラム内の任意の関数でアスセスし、使用することができる。 ローカル変数は、定義されている関数でのみ、アクセスすることができる。 グローバルとローカル変数は、同様にC言語で宣言されているが、アセンブリ言語では完全に違って見える。

グローバル変数とローカル変数のソースコードの例を示す。

int x = 1;
int y = 2;
void main()
{
x = x+y;
printf("Total = %d\n", x);
}
void main()
{
int x = 1;
int y = 2;
x = x+y;
printf("Total = %d\n", x);
}

C言語レベルでは、グローバル変数とローカル変数の差は小さく、プログラムの出力結果は同じである。 しかし、逆アセンブルした結果はかなり異なる。 グローバル変数は、メモリアドレスで参照されるが、ローカル変数はスタックアドレスで参照される。

以下のコードでは、グローバル変数xはdword_40CF60としてメモリの0x40CF60の位置にあることが示されている。 (1)でeaxの値がmovされたとき、メモリ内で値が変更される。今後、この変数を利用する全ての関数が影響を受ける。

00401003 mov eax, dword_40CF60
00401008 add eax, dword_40C000
0040100E mov dword_40CF60, eax (1)
00401013 mov ecx, dword_40CF60
00401019 push ecx
0040101A push offset aTotalD ;"total = %d\n"
0040101F call printf

ローカル変数xは、EBPに対するオフセットでスタック上に配置される。 メモリ位置[EBP-4]は、ローカル変数xを参照するために、関数内で一貫して使用される。 ebp-4という参照の方法は、定義された関数内でのみ使用可能な、スタックベースのローカル変数であると示している。

00401006 mov dword ptr [ebp-4], 0
0040100D mov dword ptr [ebp-8], 1
00401014 mov eax, [ebp-4]
00401017 add eax, [ebp-8]
0040101A mov [ebp-4], eax
0040101D mov ecx, [ebp-4]
00401020 push ecx
00401021 push offset aTotalD ; "total = %d\n"
00401026 call printf

以下のコードでは、IDAがローカル変数xにvar_4というダミーネームをつけている。 このダミーネームは、解析結果を反映した意味のある名前に変更することができる。 名前をつけることで、解析することが楽になる。

00401006 mov [ebp+var_4], 0
0040100D mov [ebp+var_8], 1
00401014 mov eax, [ebp+var_4]
00401017 add eax, [ebp+var_8]
0040101A mov [ebp+var_4], eax
0040101D mov ecx, [ebp+var_4]
00401020 push ecx
00401021 push offset aTotalD ; "total = %d\n"
00401026 call printf

算術演算の逆アセンブル

Cプログラミングでは、多くの異なるタイプの算術演算を行うことができる。 以下のコードは、Cコード言語における2つの変数の算術演算を示している。 --はデクリメント、++はインクリメントに使用される。 %は除算したあとの余りの計算(モジュロ演算)をする。

int a = 0;
int b = 1;
a = a + 11;
a = a - b;
a--;
b++;
b = a % 3;

アセンブルした結果を以下に示す。 a、bはスタックを使って参照されているため、ローカル変数である。 IDAではそれぞれvar_4とvar_8としてラベルが付けられている。 var_4は0、var_8は1に初期化される。 aは(1)でeaxにmovされ、その後eaxに0xBが加算される。これによって11となる。
(2)でa-bが計算され、(3)、(4)でデクリメントとインクリメントが行われる。
div命令かidiv命令を使うことで、除算を実行する。この命令を実行すると、eaxに結果が、edxに余りが保存される。(5)でedxをmovしているのは、余りがedxに保存されているからである。

00401006 mov [ebp+var_4], 0
0040100D mov [ebp+var_8], 1
00401014 mov eax, [ebp+var_4] (1)
00401017 add eax, 0Bh
0040101A mov [ebp+var_4], eax
0040101D mov ecx, [ebp+var_4]
00401020 sub ecx, [ebp+var_8] (2)
00401023 mov [ebp+var_4], ecx
00401026 mov edx, [ebp+var_4]
00401029 sub edx, 1 (3)
0040102C mov [ebp+var_4], edx
0040102F mov eax, [ebp+var_8]
00401032 add eax, 1 (4)
00401035 mov [ebp+var_8], eax
00401038 mov eax, [ebp+var_4]
0040103B cdq
0040103C mov ecx, 3
00401041 idiv ecx
00401043 mov [ebp+var_8], edx (5)

if文の認識

if文は、特定の条件に基づいてプログラムの実行を変更する場合に使用する。 if文は、文は、Cコードと逆アセンブリで共通している。

以下にif文のC言語のコードとアセンブリ言語のコードを示す。 (2)のjnz命令に注目してください。

条件分岐の判定は、(2)にの条件ジャンプ(jnz)で行う。 ジャンプを行うかの決定は、(1)の比較(cmp)に基づいて行われる。(1)でvar_4とvar_8が等しいかを確認している。 等しくない場合ジャンプを行い、"x is not equal to y"と表示される。等しい場合は、"x equals y."と表示され、他のセクションへジャンプする(3)。 どちらか一方のパスのみ実行される。

int x = 1;
int y = 2;
if(x == y){
    printf("x equals y.\n");
}else{
    printf("x is not equal to y.\n");
}
00401006 mov [ebp+var_8], 1
0040100D mov [ebp+var_4], 2
00401014 mov eax, [ebp+var_8]
00401017 cmp eax, [ebp+var_4] (1)
0040101A jnz short loc_40102B (2)
0040101C push offset aXEqualsY_ ; "x equals y.\n"
00401021 call printf
00401026 add esp, 4
00401029 jmp short loc_401038 (3)
0040102B loc_40102B:
0040102B push offset aXIsNotEqualToY ; "x is not equal to y.\n"
00401030 call printf

IDAのグラフィカル解析機能

IDAにはグラフビューというグラフ化ツールがある。 この機能を使うことで、条件分岐がわかりやすく表示され、リバースエンジニアリングの速度を上げることができる。 先ほどのコードも以下の図のFalse(1)、True(2)のようになる。

f:id:pinksawtooth:20150908054007p:plain

ネストされたif文

ネストされたif文も、追加された箇所を除いて通常のif文とほぼ同様になる。 以下にif文のC言語のコードとアセンブリ言語のコードを示す。

C言語では、少ししか変更がないが、アセンブリではより複雑になる。 このコードでは、三つの異なる条件ジャンプが発生する。 最初は、var_4とvar_8が等しくない場合に発生する(1)。 var_Cが0でない場合に(2)と(3)で発生する。

int x = 0;
int y = 1;
int z = 2;
if(x == y){
    if(z==0){
        printf("z is zero and x = y.\n");
    }else{
        printf("z is non-zero and x = y.\n");
    }
}else{
    if(z==0){
        printf("z zero and x != y.\n");
    }else{
        printf("z non-zero and x != y.\n");
    }
}
00401006 mov [ebp+var_8], 0
0040100D mov [ebp+var_4], 1
00401014 mov [ebp+var_C], 2
0040101B mov eax, [ebp+var_8]
0040101E cmp eax, [ebp+var_4]
00401021 jnz short loc_401047 (1)
00401023 cmp [ebp+var_C], 0
00401027 jnz short loc_401038 (2)
00401029 push offset aZIsZeroAndXY_ ; "z is zero and x = y.\n"
0040102E call printf
00401033 add esp, 4
00401036 jmp short loc_401045
00401038 loc_401038:
00401038 push offset aZIsNonZeroAndX ; "z is non-zero and x = y.\n"
0040103D call printf
00401042 add esp, 4
00401045 loc_401045:
00401045 jmp short loc_401069
00401047 loc_401047:
00401047 cmp [ebp+var_C], 0
0040104B jnz short loc_40105C (3)
0040104D push offset aZZeroAndXY_ ; "z zero and x != y.\n"
00401052 call printf
00401057 add esp, 4
0040105A jmp short loc_401069
0040105C loc_40105C:
0040105C push offset aZNonZeroAndXY_ ; "z non-zero and x != y.\n"
00401061 call printf00401061

ループの認識

ループと反復的なタスクは、すべてのソフトウェアでよく使われる。

forループの検索

forループは、Cプログラミングで使用される基本的なループメカニズムである。 forループは、4つのコンポーネントを持っている。 初期化、比較、実行命令、インクリメントかデクリメントである。

以下にif文のC言語のコードとアセンブリ言語のコードを示す。
このコードでは、iの初期値を0にセットし、100未満であるかを比較して確認する。 100未満であれば、printfが実行され、iはインクリメントされる。そしてiが100未満であるかどうかをチェックする。 iが100以上になるまで、この処理が繰り返される。

アセンブリでは、ループ用の4つの構成要素(初期化、比較、実行命令、インクリメント/デクリメント)を見つけることによって、ループを認識することができる。 (1)は初期化ステップに対応する。(2)と(3)の間のコードは、インクリメント処理に対応している。これらは、最初の実行時(4)のジャンプ命令と一緒に読み飛ばされる。 (5)で比較が行われ、結果によって(6)で条件ジャンプが派生する。 ジャンプが発生しなかった場合、printfが実行され、インクリメント処理への無条件ジャンプが行われる(7)。

int i;
for(i=0; i<100; i++)
{
    printf("i equals %d\n", i);
}
00401004 mov [ebp+var_4], 0 (1)
0040100B jmp short loc_401016 (2)
0040100D loc_40100D:
0040100D mov eax, [ebp+var_4] (3)
00401010 add eax, 1
00401013 mov [ebp+var_4], eax (4)
00401016 loc_401016:
00401016 cmp [ebp+var_4], 64h (5)
0040101A jge short loc_40102F (6)
0040101C mov ecx, [ebp+var_4]
0040101F push ecx
00401020 push offset aID ; "i equals %d\n"
00401025 call printf
0040102A add esp, 8
0040102D jmp short loc_40100D (7)

IDAのグラフビューでは、以下の図のように表示される。 f:id:pinksawtooth:20150908061707p:plain

whileループの検索

whileループは、パケットやコマンドを待機する処理でマルウェアによく使用される。 以下のコードはcheckResultから返されたステータスが0になるまでループし続ける。

インクリメント処理以外はforループのようになる。 (1)で条件ジャンプが発生し、(2)で無条件ジャンプする。 条件ジャンプが発生することで、ループから抜ける。

int status=0;
int result = 0;
while(status == 0){
    result = performAction();
    status = checkResult(result);
}
00401036 mov [ebp+var_4], 0
0040103D mov [ebp+var_8], 0
00401044 loc_401044:
00401044 cmp [ebp+var_4], 0
00401048 jnz short loc_401063 (1)
0040104A call performAction
0040104F mov [ebp+var_8], eax
00401052 mov eax, [ebp+var_8]
00401055 push eax
00401056 call checkResult
0040105B add esp, 4
0040105E mov [ebp+var_4], eax
00401061 jmp short loc_401044 (2)

関数呼び出し規約

第4章で、スタックとコール命令が関数呼び出しのために使用する方法について説明した。 関数呼び出しは、アセンブリコードによって異なって現れる。呼び出し規約は、関数呼び出しの方法を決定する。

呼び出し規約は、引数がスタック上、レジスタに配置される順番や、呼び出し元か呼び出し先のどちらが、スタックをクリーンアップするかを決めている。 呼び出し規約は、コンパイラに依存する。コンパイラによって呼び出し規約を実装する方法が。微妙にことなるため、別のコンパイラコンパイルされたコードを、解釈することは困難である。 ただし、Windows APIを使用する場合は、特定の規約に従う必要があり、互換性を持つように実装されている。

一般的な呼び出し規約は、cdecl規約、stdcall規約、fastcall規約の3つである。

cdecl

cdeclはもっともポピュラーな呼び出し規約の1つである。 cdeclでは、引数は、右から左にスタックにプッシュされ、関数が完了すると呼び出し側がスタックをクリーンアップする。戻り値は、EAXに格納される。

int test(int x, int y, int z);
int a, b, c, ret;
ret = test(a, b, c);
push c
push b
push a
call test
add esp, 12
mov ret, eax

stdcall

stdcallはcdeclに似ている。しかし、cdeclと違い、呼び出し先がスタックをクリーンアップする。呼び出し側でスタックをクリーンアップするので、add esp, 4のようなスタックを調整する命令が必要でない。代わりにtest関数のエピローグでスタックをクリーンアップする命令が必要となる。
stdcallは、Windows APIの標準呼び出し規約である。API関数を実装しているdllが、スタックをクリーンアップするので、呼び出し元でスタックを操作する必要はない。

fastcall

fastcallでは、引数(通常は2)がEDXとECXに格納される。追加の引数は右から左へロードされ、呼び出し元の関数が、スタックをクリーンアップする。コードがスタックに関与しないため、他の呼び出し規約よりも効率的である。

Push vs. Move

呼び出し規約だけでなく、コンパイラはスタック操作を行うときに命令を選択することができる。 コンパイラは、スタックにpushするのと同様の操作をmov命令ですることができる。

以下に関数呼び出しを行うCコードを示す。 関数adderは2の引数を加算して、結果を返す。main関数は、printfを使い結果を出力する。

アセンブリコードでは、arg_0とarg_4を加算し、結果をEAXに格納している。

int adder(int a, int b)
{
    return a+b;
}

void main()
{
    int x = 1;
    int y = 2;

    printf("the function returned the number %d\n", adder(x,y));
}
00401730 push ebp
00401731 mov ebp, esp
00401733 mov eax, [ebp+arg_0]
00401736 add eax, [ebp+arg_4]
00401739 pop ebp
0040173A retn

同じ呼び出し規約であっても、コンパイラが変化するとアセンブリコードが異なる。 Visual Studiogccのコードを示す。

Visual Studioでは、adderとprintfの引数は呼び出し前にpushされている。gccでは、引数はmovされている。 push命令を使用した場合、スタックポインタが変更されるので、スタックの復元をする必要がある。 しかし、movを使用した場合だとスタックポインタは変更されないので、スタックの復元の必要がない。

Visual Studio

00401746 mov [ebp+var_4], 1
0040174D mov [ebp+var_8], 2
00401754 mov eax, [ebp+var_8]
00401757 push eax
00401758 mov ecx, [ebp+var_4]
0040175B push ecx
0040175C call adder
00401761 add esp, 8
00401764 push eax
00401765 push offset TheFunctionRet
0040176A call ds:printf
gcc

00401085 mov [ebp+var_4], 1
0040108C mov [ebp+var_8], 2
00401093 mov eax, [ebp+var_8]
00401096 mov [esp+4], eax
0040109A mov eax, [ebp+var_4]
0040109D mov [esp], eax
004010A0 call adder
004010A5 mov [esp+4], eax
004010A9 mov [esp], offset TheFunctionRet
004010B0 call printf

switch文の解析

switch文は、文字または整数に基づいて決定を行うため、マルウェア作成者が使用する。 例えば、バックドアは単一のバイト値を使用し、一連の動作から動作を選択する。 switch文は、ifスタイルを使用するか、ジャンプテーブルを使用するかどちらかの方法でコンパイルされる。

ifスタイル

以下のコードは変数iを使ってswitch文を実装している。 (1)、(2)の間に条件ジャンプがある。条件ジャンプの判定は、ジャンプ命令直前の比較によって行われる。 (3)、(4)、(5)に分岐後の3つの選択肢がある。 これらのコードは互いに独立しており、コードの実行後、コードの最後へ無条件ジャンプする。

IDAのグラフビューでは、(1)、(2)、(3)のボックスが条件分岐に対応する。

switch(i)
{
  case 1:
    printf("i = %d", i+1);
    break;
  case 2:
    printf("i = %d", i+2);
    break;
  case 3:
    printf("i = %d", i+3);
    break;
  default:
    break;
}
00401013 cmp [ebp+var_8], 1
00401017 jz short loc_401027 (1)
00401019 cmp [ebp+var_8], 2
0040101D jz short loc_40103D
0040101F cmp [ebp+var_8], 3
00401023 jz short loc_401053
00401025 jmp short loc_401067 (2)
00401027 loc_401027:
00401027 mov ecx, [ebp+var_4] (3)
0040102A add ecx, 1
0040102D push ecx
0040102E push offset unk_40C000 ; i = %d
00401033 call printf
00401038 add esp, 8
0040103B jmp short loc_401067
0040103D loc_40103D:
0040103D mov edx, [ebp+var_4] (4)
00401040 add edx, 2
00401043 push edx
00401044 push offset unk_40C004 ; i = %d
00401049 call printf
0040104E add esp, 8
00401051 jmp short loc_401067
00401053 loc_401053:
00401053 mov eax, [ebp+var_4] (5)
00401056 add eax, 3
00401059 push eax
0040105A push offset unk_40C008 ; i = %d
0040105F call printf
00401064 add esp, 8

f:id:pinksawtooth:20150909034517p:plain

ジャンプテーブル

コンパイラによって最適化が行われた場合、前述のifスタイルと逆アセンブル結果が大きく異なる。 最適化されたコードでは、追加のメモリ位置へのオフセットを定義する。 スイッチ変数は、ジャンプテーブル(2)へのインデックスとして使用する。 この例では、excにスイッチ変数が入っている。 スイッチケースは4つあるが、ジャンプテーブルの範囲は0~3のためsub ecx, 1によってインデックスして使用できるように調整する。 スイッチ変数によって(1)で対応するコードへジャンプする。 ジャンプテーブルの各エントリのサイズが4バイトのアドレスであるため、EDXが4で乗算される。

IDAのグラフビューでは、以下の図のように表示される。

switch(i)
{
  case 1:
    printf("i = %d", i+1);
    break;
  case 2:
    printf("i = %d", i+2);
    break;
  case 3:
    printf("i = %d", i+3);
    break;
  case 4:
    printf("i = %d", i+3);
    break;
  default:
    break;
}
00401016 sub ecx, 1
00401019 mov [ebp+var_8], ecx
0040101C cmp [ebp+var_8], 3
00401020 ja short loc_401082
00401022 mov edx, [ebp+var_8]
00401025 jmp ds:off_401088[edx*4] (1)
0040102C loc_40102C:
...
00401040 jmp short loc_401082
00401042 loc_401042:
...
00401056 jmp short loc_401082
00401058 loc_401058:
...
0040106C jmp short loc_401082
0040106E loc_40106E:
...
00401082 loc_401082:
00401082 xor eax, eax
00401084 mov esp, ebp
00401086 pop ebp
00401087 retn
00401087 _main endp
00401088 (2)off_401088 dd offset loc_40102C
0040108C dd offset loc_401042
00401090 dd offset loc_401058
00401094 dd offset loc_40106E

f:id:pinksawtooth:20150909041306p:plain

配列の逆アセンブル

マルウェアでは、通信先のホスト名などを含む文字列へのポインタ配列を使用する。
以下のプログラムでは、2つの配列を定義する。両方の配列ともループの中で値を設定されている。 配列aはローカル変数として定義され、配列bはグローバル変数として定義されている。 これらの定義の違いは逆アセンブル結果に影響する。

アセンブリでは、配列は開始点となるベースアドレスを使用してアクセスされる。 各要素の大きさは明確ではないが、配列がどのようにインデックスされているかを見ると決定できる。 このコードでは、配列aのベースアドレスはvar_14に、配列bのベースアドレスはdword_40A000に対応している。 両方の配列とも整数の配列であり、各要素サイズは4である。 各配列にアクセスするための命令(1)、(2)が異なる。 両方の場合において、ECXはインデックスとして使用され、要素のサイズを考慮するために4を乗算している。 結果の値は、適切な配列要素にアクセスするために、配列のベースアドレスに加算されている。

int b[5] = {123,87,487,7,978};
void main()
{
  int i;
  int a[5];

  for(i = 0; i<5; i++)
  {
    a[i] = i;
    b[i] = i;
  }
}
00401006 mov [ebp+var_18], 0
0040100D jmp short loc_401018
0040100F loc_40100F:
0040100F mov eax, [ebp+var_18]
00401012 add eax, 1
00401015 mov [ebp+var_18], eax
00401018 loc_401018:
00401018 cmp [ebp+var_18], 5
0040101C jge short loc_401037
0040101E mov ecx, [ebp+var_18]
00401021 mov edx, [ebp+var_18]
00401024 mov [ebp+ecx*4+var_14], edx (1)
00401028 mov eax, [ebp+var_18]
0040102B mov ecx, [ebp+var_18]
0040102E mov dword_40A000[ecx*4], eax (2)
00401035 jmp short loc_40100F

構造体の識別

構造体の構造は、配列に似ているが、異なる種類の要素を含む。 構造体は、マルウェアでは、グループ情報に使用される。多くの関数が同じグループの変数にアクセスする場合、独立した異なる変数を保持するよりも、構造体を使用するほうが簡単である。(Windows APIは多くの場合、構造体を使用する)

以下のコードでは、整数配列、文字、ダブルで構成された構造を定義している(1)。メイン関数では、構造体のメモリを割り当て、テスト関数に構造体を渡している。(2)で定義された構造体gmsグローバル変数である。

構造体は、開始点となるベースアドレスを使用してアクセスされる。 構造体は、近くのデータタイプが同じ構造の一部であるかどうかを判断するのが難しい。 構造を特定する能力は、マルウェアを解析する能力に大きく関係する。

アセンブリコードでは、gms構造体はグローバル変数であるため、ベースアドレスはdword_40EA30となる。 この構造体のベースアドレスが(1)でpushされ、sub_401000(テスト)関数に渡される。

arg_0は、構造のベースアドレスである。 オフセット0x14は、構造体の中で文字を格納する。そして0x61はASCIIコードのaに対応している。 (1)で浮動小数点命令の一部として使用されていることからオフセット0x18はdoubleである。 オフセット0x0, 0x4, 0x8, 0xC, 0x10は、forループの中で整数値がmovされて(2)でアクセスされている。 IDAを使い、構造体を定義することで、ディスアセンブルしたコードを読みやすくする。

struct my_structure { (1)
    int x[5];
    char y;
    double z;
};
struct my_structure *gms; (2)

void test(struct my_structure *q)
{
    int i;
    q->y = 'a';
    q->z = 15.6;
    for(i = 0; i<5; i++){
        q->x[i] = i;
    }
}

void main()
{
    gms = (struct my_structure *) malloc(
    sizeof(struct my_structure));
    test(gms);
}
00401050 push ebp
00401051 mov ebp, esp
00401053 push 20h
00401055 call malloc
0040105A add esp, 4
0040105D mov dword_40EA30, eax
00401062 mov eax, dword_40EA30
00401067 push eax (1)
00401068 call sub_401000
0040106D add esp, 4
00401070 xor eax, eax
00401072 pop ebp
00401073 retn
00401000 push ebp
00401001 mov ebp, esp
00401003 push ecx
00401004 mov eax,[ebp+arg_0]
00401007 mov byte ptr [eax+14h], 61h
0040100B mov ecx, [ebp+arg_0]
0040100E fld ds:dbl_40B120 (1)
00401014 fstp qword ptr [ecx+18h]
00401017 mov [ebp+var_4], 0
0040101E jmp short loc_401029
00401020 loc_401020:
00401020 mov edx,[ebp+var_4]
00401023 add edx, 1
00401026 mov [ebp+var_4], edx
00401029 loc_401029:
00401029 cmp [ebp+var_4], 5
0040102D jge short loc_40103D
0040102F mov eax,[ebp+var_4]
00401032 mov ecx,[ebp+arg_0]
00401035 mov edx,[ebp+var_4]
00401038 mov [ecx+eax*4],edx (2)
0040103B jmp short loc_401020
0040103D loc_40103D:
0040103D mov esp, ebp
0040103F pop ebp
00401040 retn

リンクドリストのトラバーサルの解析

リンクドリストは、データレコードの配列からなるデータ構造である。 各レコードは、シーケンス内の次のレコードへの参照(リンク)が含まれているフィールドを持っている。 アレイがある上でリンクされたリストを使用することの主な利点は、リンクされた項目の順序は、データ項目がメモリまたはディスク上に格納された順序と変更できる点である。つまり、リンクドリストでは、リスト内の任意の点で挿入とノードの除去が可能である。

以下のリンクドリストは、pnodeという名前の構造体のノードで構成されていて、それが二つのループで操作される。 最初のループは10個のノードを作成し、データを入れる。(1) 次のループは、ノードの内容を全て出力する。(2)

ディスアセンブルしたコードを理解するためには、mainメソッド内の2つのコード構成を識別することが重要である。 まず、forループを識別する。 var_Cは、ループのカウンタでiにvar_8は変数headに、var_4は変数currに対応している。(1)(2) whileループは(3)(5)で、繰り返し実行している。 ループ内のva_4は次のレコードに設定されている。(4)

リンクされたリストを認識するには、同じタイプの別のオブジェクトを指すポインタが含まれていることを認識しなければならない。 (4)で、var_4は、[eax+4]が代入されている。eaxは前のvar_4からきた値が入っている。 これは、構造体var_4に4バイトの別の構造体へのポインタが含まれているということである。

struct node
{
  int x;
  struct node * next;
};

typedef struct node pnode;

void main()
{
  pnode * curr, * head;
  int i;
  head = NULL;
  
  for(i=1;i<=10;i++) (1)
    {
    curr = (pnode *)malloc(sizeof(pnode));
    curr->x = i;
    curr->next = head;
    head = curr;
  }

  curr = head;
  
  while(curr) (2)
    {
    printf("%d\n", curr->x);
    curr = curr->next ;
    }
}
0040106A mov [ebp+var_8], 0
00401071 mov [ebp+var_C], 1
00401078
00401078 loc_401078:
00401078 cmp [ebp+var_C], 0Ah
0040107C jg short loc_4010AB
0040107E mov [esp+18h+var_18], 8
00401085 call malloc
0040108A mov [ebp+var_4], eax
0040108D mov edx, [ebp+var_4]
00401090 mov eax, [ebp+var_C]
00401093 mov [edx], eax (1)
00401095 mov edx, [ebp+var_4]
00401098 mov eax, [ebp+var_8]
0040109B mov [edx+4], eax (2)
0040109E mov eax, [ebp+var_4]
004010A1 mov [ebp+var_8], eax
004010A4 lea eax, [ebp+var_C]
004010A7 inc dword ptr [eax]
004010A9 jmp short loc_401078
004010AB loc_4010AB:
004010AB mov eax, [ebp+var_8]
004010AE mov [ebp+var_4], eax
004010B1
004010B1 loc_4010B1:
004010B1 cmp [ebp+var_4], 0 (3)
004010B5 jz short locret_4010D7
004010B7 mov eax, [ebp+var_4]
004010BA mov eax, [eax]
004010BC mov [esp+18h+var_14], eax
004010C0 mov [esp+18h+var_18], offset aD ; "%d\n"
004010C7 call printf
004010CC mov eax, [ebp+var_4]
004010CF mov eax, [eax+4]
004010D2 mov [ebp+var_4], eax (4)
004010D5 jmp short loc_4010B1 (5)