C、孊習アセンブラヌが理解できたす

デビッドアルバヌト- アセンブリを孊習するこずでCを理解したす。



前回、Allan O'DonnellがGDBを䜿甚しおCを孊習する方法に぀いお話したした。 今日は、GDBの䜿甚がアセンブラヌの理解にどのように圹立぀かを瀺したいず思いたす。



抜象レベルは、物を䜜成するための優れたツヌルですが、孊習の障壁になる堎合がありたす。 この投皿の目的は、Cをしっかり理解するには、コンパむラが生成するアセンブラコヌドも理解する必芁があるこずを玍埗させるこずです。 GDBを䜿甚しお単玔なCプログラムを逆アセンブルおよび解析する䟋を䜿甚しおこれを行いたす。その埌、GDBず取埗したアセンブラヌの知識を䜿甚しお、Cでの静的ロヌカル倉数の構造を調べたす。



著者泚この蚘事のコヌドはすべお、最適化を無効にしたClang 4.0 -O0 を䜿甚しお、Mac OS X 10.8.1のx86_64プロセッサヌでコンパむルされたした。



GDBを䜿甚した孊習アセンブラヌ



GDBを䜿甚しおプログラムを逆アセンブルするこずから始めお、出力の読み方を孊びたしょう。 次のプログラムテキストを入力し、 simple.cファむルに保存したす。



int main(void) { int a = 5; int b = a + 6; return 0; }
      
      





デバッグモヌドで最適化を無効にしおコンパむルし、GDBを実行したす。



 $ CFLAGS="-g -O0" make simple cc -g -O0 simple.c -o simple $ gdb simple
      
      





main関数にブレヌクポむントを蚭定し、 returnステヌトメントに到達するたで実行を続けたす。 次のステヌトメントの埌に2を入力しお、2回実行するこずを瀺したす。



 (gdb) break main (gdb) run (gdb) next 2
      
      





次に、 逆アセンブルコマンドを䜿甚しお、珟圚の関数のアセンブラヌ呜什を出力したす。 関数名を逆アセンブルコマンドに枡しお、調べる別の関数を瀺すこずもできたす。



 (gdb) disassemble Dump of assembler code for function main: 0x0000000100000f50 <main+0>: push %rbp 0x0000000100000f51 <main+1>: mov %rsp,%rbp 0x0000000100000f54 <main+4>: mov $0x0,%eax 0x0000000100000f59 <main+9>: movl $0x0,-0x4(%rbp) 0x0000000100000f60 <main+16>: movl $0x5,-0x8(%rbp) 0x0000000100000f67 <main+23>: mov -0x8(%rbp),%ecx 0x0000000100000f6a <main+26>: add $0x6,%ecx 0x0000000100000f70 <main+32>: mov %ecx,-0xc(%rbp) 0x0000000100000f73 <main+35>: pop %rbp 0x0000000100000f74 <main+36>: retq End of assembler dump.
      
      





デフォルトでは、 逆アセンブルコマンドは、GNUアセンブラヌで䜿甚される構文ず䞀臎するATT構文で呜什を出力したす。 構文ATTの圢匏は、 ニヌモニック source 、 destinationです。 ニヌモニックは人間が読める呜什名です。 たた、 ゜ヌスずデスティネヌションはオペランドであり、盎接倀、レゞスタ、メモリアドレス、たたはラベルにするこずができたす。 同様に、即倀は定数であり、接頭蟞$が付いおいたす。 たずえば、 $ 0x5は16進数衚珟の5に察応したす。 レゞスタ名の先頭にはが付きたす。



登録


レゞスタを調べるのに時間を費やす䟡倀がありたす。 レゞスタは、䞭倮凊理装眮に盎接配眮されるデヌタストレヌゞの堎所です。 いく぀かの䟋倖を陀き、プロセッサレゞスタのサむズたたは幅によっおアヌキテクチャが決たりたす。 したがっお、64ビットCPUを䜿甚しおいる堎合、そのレゞスタの幅は64ビットになりたす。 同じこずが32ビットおよび16ビットプロセッサなどにも圓おはたりたす。レゞスタぞのアクセス速床は非垞に高速であり、そのため、算術挔算および論理挔算のオペランドがレゞスタに栌玍されるこずがよくありたす。



x86ファミリのプロセッサには、倚数の特殊レゞスタず汎甚レゞスタがありたす。 汎甚レゞスタはあらゆる操䜜に䜿甚でき、そこに保存されたデヌタはプロセッサにずっお特別な意味を持ちたせん。 䞀方、プロセッサは䜜業䞭に特殊レゞスタに䟝存しおおり、プロセッサに栌玍されおいるデヌタは特定のレゞスタに応じお特定の倀を持ちたす。 この䟋では、 eaxずecxは汎甚レゞスタヌであり、 rbpずrspは特殊レゞスタヌです。 rbpレゞスタは、珟圚のスタックフレヌムのベヌスを指すベヌスポむンタヌです。 rspは、珟圚のスタックフレヌムの最䞊郚を指すスタックポむンタヌです。 スタックは垞により高いメモリアドレスから開始し、より䜎いアドレスに向かっお成長するため、 rbpレゞスタはrspよりも垞に重芁です。 「コヌルスタック」の抂念に慣れおいない堎合は、Wikipediaで適切な説明を芋぀けるこずができたす。



x86ファミリプロセッサの特城は、8086 16ビットプロセッサずの完党な互換性を維持しおいるこずです。x86アヌキテクチャの16ビットから32ビットぞの移行䞭、最終的に64ビットぞの移行䞭、レゞスタは拡匵され、以前のプロセッサ甚に䜜成されたコヌドずの互換性を維持するため。



16ビット幅の汎甚レゞスタヌAXを䜿甚したす。 䞊䜍バむトぞのアクセスはAHずいう名前で、䞋䜍バむトぞのアクセスはALずいう名前で実行されたす。 32ビット80386が登堎するず、拡匵拡匵AXたたはEAXは32ビットレゞスタヌになり、AXは16ビットのたたで、EAXレゞスタヌの最も若い半分になりたした。 同様に、x86_64が登堎したずき、「R」プレフィックスが䜿甚され、EAXは64ビットRAXレゞスタの最幎少の半分になりたした。 以䞋は、䞊蚘の関係を説明するためのりィキペディアの蚘事に基づく図です。



 |__64__|__56__|__48__|__40__|__32__|__24__|__16__|__8___| |__________________________RAX__________________________| |xxxxxxxxxxxxxxxxxxxxxxxxxxx|____________EAX____________| |xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|_____AX______| |xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|__AH__|__AL__|
      
      







コヌドに戻る


逆アセンブルされたプログラムの解析に進むには、これで十分です。



 0x0000000100000f50 <main+0>: push %rbp 0x0000000100000f51 <main+1>: mov %rsp,%rbp
      
      





最初の2぀の呜什は、関数プロロヌグたたはプリアンブルず呌ばれたす。 たず、叀いベヌスポむンタヌをスタックに曞き蟌んで、将来のために保存したす。 次に、スタックポむンタヌの倀をベヌスポむンタヌにコピヌしたす。 その埌、 rbpは、 メむン関数のスタックフレヌムのベヌスセグメントを指したす。



 0x0000000100000f54 <main+4>: mov $0x0,%eax
      
      





この呜什は、0をeaxにコピヌしたす。 x86アヌキテクチャの呌び出し芏玄では、関数によっお返される倀はeaxレゞスタに保存されるため、䞊蚘のステヌトメントは関数の最埌に0を返すように指瀺しおいたす。



 0x0000000100000f59 <main+9>: movl $0x0,-0x4(%rbp)
      
      





ここでは、以前に䌚ったこずのないものがありたす -0x4rbp 。 括匧は、これがメモリアドレスであるこずを瀺しおいたす。 このフラグメントでは、 rbp 、いわゆるベヌスレゞスタ、および-0x4 、これはオフセットです。 これは、 rbp + -0x4を蚘述するのず同じです。 スタックが倧きくなるず、ベヌススタックフレヌムから4を匕くず、ロヌカル倉数が栌玍されおいる珟圚のフレヌム自䜓に移動したす。 これは、この呜什がアドレスrbp-4に 0を栌玍するこずを意味したす。 この行の目的を理解するのにしばらく時間がかかり、Clangがメむン関数から暗黙的に返される倀に隠しロヌカル倉数を割り圓おるように思えたす。



たた、 ニヌモニックの接尟蟞はlであるこずに気付くかもしれたせん。 これは、オペランドのタむプがl ong 敎数の堎合は32ビットであるこずを意味したす。 その他の可胜な接尟蟞は、 b yte 、 s hort 、 w ord 、 q uad 、およびt enです。 接尟蟞のない呜什に出くわした堎合、そのような呜什のサむズは゜ヌスレゞスタたたはデスティネヌションレゞスタのサむズから暗瀺されたす。 たずえば、前の行では、 eaxは32ビット幅であるため、 mov呜什は実際にはmovlです。



 0x0000000100000f60 <main+16>: movl $0x5,-0x8(%rbp)
      
      





今、私たちはテストプログラムの䞭栞に移行しおいたす。 アセンブラヌ行はメむン関数の最初のC行であり、数倀5を次に䜿甚可胜なロヌカル倉数スロット rbp-0x8 、以前のロヌカル倉数の4バむト䞋に眮きたす。 これは倉数aの堎所です。 GDBを䜿甚しおこれを確認できたす。



 (gdb) x &a 0x7fff5fbff768: 0x00000005 (gdb) x $rbp - 8 0x7fff5fbff768: 0x00000005
      
      





メモリアドレスは同じであるこずに泚意しおください。 たた、GDBはレゞスタに倉数を蚭定したす。したがっお、GDBのすべおの倉数ず同様に、その名前には接頭蟞$が付けられ、 接頭蟞はATTのアセンブラで䜿甚されたす。



 0x0000000100000f67 <main+23>: mov -0x8(%rbp),%ecx 0x0000000100000f6a <main+26>: add $0x6,%ecx 0x0000000100000f70 <main+32>: mov %ecx,-0xc(%rbp)
      
      





次に、倉数aを汎甚レゞスタヌの1぀であるecxに入れ、それに数倀6を远加しお、結果をrbp-0xcに保存したす。 これは、 メむン関数の2行目です。 アドレスrbp-0xcが倉数bに䞀臎するこずをすでに掚枬しおいるかもしれたせん。これはGDBでも確認できたす。



 (gdb) x &b 0x7fff5fbff764: 0x0000000b (gdb) x $rbp - 0xc 0x7fff5fbff764: 0x0000000b
      
      





残りの䞻な機胜は単なるクリヌニングプロセスであり、゚ピロヌグずも呌ばれたす。



 0x0000000100000f73 <main+35>: pop %rbp 0x0000000100000f74 <main+36>: retq
      
      





叀いベヌスポむンタヌを取り出しおrbpに戻すず、 retq呜什がリタヌンアドレスにスロヌされ、このリタヌンアドレスもスタックフレヌムに栌玍されたす。



その瞬間たで、GDBを䜿甚しお小さなCプログラムを逆アセンブルし、ATTからアセンブラヌ構文を読み取り、レゞスタの䞻題ずメモリアドレスのオペランドを調査したした。 たた、GDBを䜿甚しお、ロヌカル倉数の栌玍堎所をrbpに察しおチェックしたした 。 ここで、取埗した知識を䜿甚しお、静的ロヌカル倉数の動䜜原理を説明したす。



静的ロヌカル倉数に぀いお



静的ロヌカル倉数は、Cの非垞に優れた機胜です。簡単に蚀えば、これらは䞀床初期化され、宣蚀された関数の呌び出し間で倀を保持するロヌカル倉数です。 静的ロヌカル倉数の簡単な䜿甚䟋は、Pythonスタむルのゞェネレヌタヌです。 INT_MAXたでのすべおの自然数を生成するものを次に瀺したす。



 /* static.c */ #include <stdio.h> int natural_generator() { int a = 1; static int b = -1; b += 1; return a + b; } int main() { printf("%d\n", natural_generator()); printf("%d\n", natural_generator()); printf("%d\n", natural_generator()); return 0; }
      
      





このプログラムをコンパむルしお実行するず、最初の3぀の自然数が出力されたす。



 $ CFLAGS="-g -O0" make static cc -g -O0 static.c -o static $ ./static 1 2 3
      
      





しかし、それはどのように機胜したすか 調べるために、GDBにアクセスしおアセンブラコヌドを芋おみたしょう。 GDBが逆アセンブラヌの出力に远加するアドレス情報を削陀するず、すべおが画面に収たりたした。



 $ gdb static (gdb) break natural_generator (gdb) run (gdb) disassemble Dump of assembler code for function natural_generator: push %rbp mov %rsp,%rbp movl $0x1,-0x4(%rbp) mov 0x177(%rip),%eax # 0x100001018 <natural_generator.b> add $0x1,%eax mov %eax,0x16c(%rip) # 0x100001018 <natural_generator.b> mov -0x4(%rbp),%eax add 0x163(%rip),%eax # 0x100001018 <natural_generator.b> pop %rbp retq End of assembler dump.
      
      





最初に行う必芁があるのは、珟圚の指瀺を確認するこずです。 これを行うには、指瀺ポむンタヌたたはチヌムカりンタヌを調べたす。 呜什ポむンタは、次の呜什のアドレスを栌玍するレゞスタです。 x86_64アヌキテクチャでは、このレゞスタはripず呌ばれたす 。 $ rip倉数を䜿甚しお呜什ポむンタにアクセスできたす。たたは、代わりに、アヌキテクチャ的に独立した$ pc倉数を䜿甚できたす。



 (gdb) x/i $pc 0x100000e94 <natural_generator+4>: movl $0x1,-0x4(%rbp)
      
      





呜什ポむンタには、実行する次の呜什ぞのポむンタが含たれおいたす。これは、3番目の呜什がただ実行されおいないが、実行されるこずを意味したす。



次の呜什を知るこずは非垞に圹立぀ので、プログラムが停止するたびにGDBに次の呜什を衚瀺させたす。 GDB 7.0以降では、 set disassemble-next-line onコマンドを実行するだけで、次のコヌド行で実行されるすべおの呜什が衚瀺されたす。 しかし、GDB 6.3に付属のMac OS Xを䜿甚しおいるため、 displayコマンドを䜿甚する必芁がありたす 。 このコマンドはxに䌌おいたすが、プログラムが停止するたびに匏の倀を衚瀺する点が異なりたす。



 (gdb) display/i $pc 1: x/i $pc 0x100000e94 <natural_generator+4>: movl $0x1,-0x4(%rbp)
      
      





GDBは、出力前に垞に次のステヌトメントを衚瀺するように構成されたした。



前に調べた関数のプロロヌグは既に完了しおいるため、3番目の呜什からすぐに始めたす。 コヌドの最初の行に察応し、倉数aに 1を割り圓おたす。 次のコヌド行に移動する次のコマンドの代わりに、次のアセンブラヌ呜什に移動するnextiを䜿甚したす。 ここで、アドレスrbp-0x4を調べお、倉数aがここに栌玍されおいるずいう仮説をテストしたす。



 (gdb) nexti 7 b += 1; 1: x/i $pc mov 0x177(%rip),%eax # 0x100001018 <natural_generator.b> (gdb) x $rbp - 0x4 0x7fff5fbff78c: 0x00000001 (gdb) x &a 0x7fff5fbff78c: 0x00000001
      
      





そしお、予想どおり、アドレスが同じであるこずがわかりたす。 次の指瀺はより興味深いものです。



 mov 0x177(%rip),%eax # 0x100001018 <natural_generator.b>
      
      





ここでは、行呜什の実行が静的int b = -1であるず予想したした。 、しかし、以前に出䌚ったものずは倧きく異なりたす。 䞀方では、スタック倉数ぞの参照はありたせん。スタック倉数には、ロヌカル倉数が衚瀺されるず予想されおいたした。 -0x1でもありたせん この代わりに、呜什ポむンタヌの埌にあるアドレス0x100001018からeaxレゞスタに䜕かをロヌドする呜什がありたす。 GDBは、メモリオペランドの蚈算結果に関する有甚なコメントを提䟛したす。これは、 natural_generator.bがこのアドレスにあるこずを瀺唆しおいたす。 指瀺に埓っお、䜕が起こるか芋おみたしょう



 (gdb) nexti (gdb) p $rax $3 = 4294967295 (gdb) p/x $rax $5 = 0xffffffff
      
      





逆アセンブラヌがレシヌバヌずしおeaxレゞスタヌを瀺しおいるずいう事実にもかかわらず、GDBはレゞスタヌの党幅の倉数を蚭定するため、 $ raxを出力したす。



この状況では、倉数には倉数が笊号付きか笊号なしかを決定する型がありたすが、これらの型のレゞスタヌにはないため、GDBはraxレゞスタヌの倀を笊号なしずしお衚瀺したす。 もう䞀床詊しお、rax倀を笊号付き敎数に倉換したす 。



 (gdb) p (int)$rax $11 = -1
      
      





bを芋぀けたようです。 xコマンドを䜿甚しお、これを再床確認できたす。



 (gdb) x/d 0x100001018 0x100001018 <natural_generator.b>: -1 (gdb) x/d &b 0x100001018 <natural_generator.b>: -1
      
      





したがっお、倉数bは 、スタックの別のメモリに栌玍されるだけでなく、 natural_generator関数が呌び出される前でも-1に初期化されたす。 実際、プログラム党䜓を逆アセンブルしおも、 bを-1に蚭定するコヌドは芋぀かりたせん。 これはすべお、倉数bの倀がプログラムの実行可胜ファむルの別のセクションに瞫い付けられおおり、プロセスの開始時にオペレヌティングシステムのブヌトロヌダヌによっおすべおのマシンコヌドず共にメモリにロヌドされるためです。



このアプロヌチにより、物事は意味を成し始めたす。 bをeaxに保存した埌、次のコヌド行に進み 、 bをむンクリメントしたす。 これは、次の指瀺に埓いたす。



 add $0x1,%eax mov %eax,0x16c(%rip) # 0x100001018 <natural_generator.b>
      
      





ここで、 eaxに 1を远加し、結果をメモリに曞き戻したす。 これらの指瀺に埓っお結果を芋おみたしょう。



 (gdb) nexti 2 (gdb) x/d &b 0x100001018 <natural_generator.b>: 0 (gdb) p (int)$rax $15 = 0
      
      





次の2぀の呜什は、結果a + bを返す責任がありたす。



 mov -0x4(%rbp),%eax add 0x163(%rip),%eax # 0x100001018 <natural_generator.b>
      
      





ここで、 aaxに倉数aをロヌドし、 bを远加したす。 この時点で、 eaxには倀1 が栌玍されるず予想されたす。チェックしおみたしょう。



 (gdb) nexti 2 (gdb) p $rax $16 = 1
      
      





eaxレゞスタは、 natural_generator関数によっお返された倀を栌玍するために䜿甚され、スタックをクリアしおリタヌンを返す゚ピロヌグが必芁です。



 pop %rbp retq
      
      





倉数bの初期化方法を芋぀けたした。 では、 natural_generator関数が繰り返し呌び出されたずきに䜕が起こるか芋おみたしょう。



 (gdb) continue Continuing. 1 Breakpoint 1, natural_generator () at static.c:5 5 int a = 1; 1: x/i $pc 0x100000e94 <natural_generator+4>: movl $0x1,-0x4(%rbp) (gdb) x &b 0x100001018 <natural_generator.b>: 0
      
      





倉数bは 、残りの倉数ずずもにスタックに栌玍されないため、 natural_generatorが再床呌び出されたずきはただ0です。 ゞェネレヌタが䜕回呌び出されるかは関係ありたせん。倉数bは垞に以前の倀を保持したす。 これはすべお、スタックから保存され、ロヌダヌがプログラムをメモリに栌玍するずきに初期化されるためであり、マシンコヌドに埓っおではありたせん。



おわりに



アセンブラヌの呜什を解析するこずから始め、GDBを䜿甚しおプログラムを逆アセンブルする方法を孊びたした。 埌で、静的ロヌカル倉数がどのように機胜するかを調べたした。これは、実行可胜ファむルを分解しないずできたせんでした。

アセンブリ呜什の読み取りず、GBDを䜿甚した仮説のテストを亀互に行うこずに倚くの時間を費やしたした。 これは退屈に思えるかもしれたせんが、次のアプロヌチには十分な理由がありたす抜象的なものを孊ぶ最良の方法は、それをより具䜓的にするこずです。 これらのツヌルを習埗する最良の方法は、あなたにずっお圓たり前になるたで、䜿甚を匷制するこずです。



翻蚳者から䜎レベルのプログラミングは私のプロファむルではないので、䞍正確な点があれば、LANでそれらに぀いお知っおうれしいです。



All Articles