GDBを使用してCを学ぶ

Allan O'Donnell Learning CによるGDBによる記事の翻訳。



Ruby、Scheme、Haskellなどの高レベル言語の機能に基づいて、Cの学習は困難な作業になる可能性があります。 手動のメモリ管理やポインターなど、Cの低レベルの機能を克服することに加えて、 REPLを使用しないで行う必要があります。 REPLでプログラミングの研究に慣れると、書き込み-コンパイル-実行サイクルを処理することは少しがっかりします。



最近、Cの擬似REPLとしてGDBを使用できることがわかりました。デバッグだけでなく、言語を学習するためのツールとしてGDBを使用して実験しましたが、とても楽しいことがわかりました。



この投稿の目的は、GDBがCについて学ぶための優れたツールであることを示すことです。お気に入りのGDBコマンドを紹介し、GDBを使用してCの難しい部分の1つを理解する方法を示します。ポインター。



GDBの概要



以下の小さなCプログラムを作成することから始めましょう-minimal.c



int main() { int i = 1337; return 0; }
      
      





プログラムはまったく何もせず、単一のprintfコマンドさえも持たないことに注意してください。 GBDを使用してCを学習する新しい世界に飛び込みましょう。



このプログラムを-gフラグを付けてコンパイルし、GDBが機能するデバッグ情報を生成し、同じ情報をスローします。



 $ gcc -g minimal.c -o minimal $ gdb minimal
      
      





これで、GDBのコマンドラインに即座にアクセスできるはずです。 REPLを約束しました。



 (gdb) print 1 + 2 $1 = 3
      
      





すごい printは、C番目の式の結果を評価する組み込みGDBコマンドです。 特定のGDBコマンドが何をしているのかわからない場合は、ヘルプを使用します。GDBコマンドプロンプトで「コマンド名」と入力します。



より興味深い例を次に示します。



 (gbd) print (int) 2147483648 $2 = -2147483648
      
      





2147483648 == -2147483648である理由の説明を見逃します。 ここでの主要なポイントは、算術でさえCで潜行的であり、GDBはCの算術を理解するということです。



それでは、 メイン関数にブレークポイントを設定してプログラムを実行しましょう。



 (gdb) break main (gdb) run
      
      





プログラムは3行目で停止し、 i変数が正確に初期化されました。 興味深いことに、変数はまだ初期化されていませんが、 printコマンドを使用してその値を確認できます。



 (gdb) print i $3 = 32767
      
      





Cでは、ローカルの初期化されていない変数の値は定義されていないため、受け取る結果は異なる場合があります。



次のコマンドを使用して、現在のコード行を実行できます。



 (gdb) next (gdb) print i $4 = 1337
      
      





Xコマンドを使用したメモリの探索



Cの変数は、連続したメモリブロックです。 各変数のブロックは、2つの数値で特徴付けられます。



1.ブロックの最初のバイトの数値アドレス。

2.バイト単位のブロックサイズ。 このサイズは、変数のタイプによって決まります。



C言語の際立った特徴の1つは、可変メモリブロックに直接アクセスできることです。 演算子はメモリ内の変数のアドレスを提供し、 sizeofはメモリ変数が占めるサイズを計算します。



GDBの両方の機能で遊ぶことができます:



 (gdb) print &i $5 = (int *) 0x7fff5fbff584 (gdb) print sizeof(i) $6 = 4
      
      





通常の言語では、これは変数i0x7fff5fbff5b4にあり、メモリで4バイトを占有することを意味します。



メモリ内の変数のサイズはその型に依存することは既に述べましたが、一般的に言って、 sizeof演算子はデータ型自体にも作用できます。



 (gdb) print sizeof(int) $7 = 4 (gdb) print sizeof(double) $8 = 8
      
      





つまり、少なくとも私のマシンでは、 int型の変数は4バイトを占有し、 double型の変数は8バイトを占有します。



GDBには、メモリを直接探索するための強力なツールであるxコマンドがあります。 このコマンドは、特定のアドレスから始まるメモリをチェックします。 また、チェックするバイト数、およびそれらを表示するフォームを正確に制御する多くのフォーマットコマンドがあります。 問題がある場合は、GDBコマンドプロンプトでhelp xと入力します。



既に知っているように、 演算子は変数のアドレスを計算します。つまり、 xコマンドを値&iに渡して、変数iの後ろに隠された個々のバイトを見る機会を得ることができます。



 (gdb) x/4xb &i 0x7fff5fbff584: 0x39 0x05 0x00 0x00
      
      





フォーマットフラグは、16進数(he x )バイトバイト( b yte)で出力される4つの値( 4 )を取得したいことを示します。 i変数は正確に多くのメモリを占有するため、4バイトのみをチェックすることを示しました。 出力には、メモリ内の変数のバイト表現が表示されます。



しかし、バイト単位の出力には、常に注意が必要な微妙な点が1つあります。Intelマシンでは、バイトは「 最低から最高 」の順序(右から左)で保存されます。終了(左から右へ)。



この問題を明確にする1つの方法は、変数iにさらに興味深い値を割り当て、このメモリの一部を再度チェックすることです。



 (gdb) set var i = 0x12345678 (gdb) x/4xb &i 0x7fff5fbff584: 0x78 0x56 0x34 0x12
      
      





ptypeメモリの探索



ptypeチームはおそらく私のお気に入りの1つです。 Cth式のタイプを示します。



 (gdb) ptype i type = int (gdb) ptype &i type = int * (gdb) ptype main type = int (void)
      
      





Cの型は複雑になる可能性がありますが、 ptypeを使用すると、それらをインタラクティブに探索できます。



ポインターと配列



配列はCの驚くほど微妙な概念です。この段落の本質は、単純なプログラムを作成し、配列が意味をなすまでGDBで実行することです。



したがって、 array.cを使用したプログラムコードが必要です



 int main() { int a[] = {1, 2, 3}; return 0; }
      
      





-gフラグを使用してコンパイルし、GDBで実行し、 次に初期化行に移動します。



 $ gcc -g arrays.c -o arrays $ gdb arrays (gdb) break main (gdb) run (gdb) next
      
      





この段階で、変数の内容を表示し、そのタイプを見つけることができます。



 (gdb) print a $1 = {1, 2, 3} (gdb) ptype a type = int [3]
      
      





プログラムがGDBで適切に構成されたので、最初に行うべきことは 「内部」変数どのように見えるかを確認するためにxコマンドを使用することです。



 (gdb) x/12xb &a 0x7fff5fbff56c: 0x01 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x7fff5fbff574: 0x03 0x00 0x00 0x00
      
      





これは、配列aのメモリ位置が0x7fff5fbff56cで始まることを意味します。 最初の4バイトには[0] 、次の4バイトには[1] 、最後の4バイトには[2]が含まれます。 実際、 sizeofがメモリ内の正確に12バイトを占有していることを確認して確認できます。



 (gdb) print sizeof(a) $2 = 12
      
      





この時点まで、配列はあるべき姿に見えます。 配列に対応する型を持ち、すべての値をメモリの隣接するセクションに保存します。 ただし、特定の状況では、配列はポインターのように動作します! たとえば、算術をaに適用できます。



 (gdb) print a + 1 $3 = (int *) 0x7fff5fbff570
      
      





通常、これは、 a + 1がアドレス0x7fff5fbff570を持つintへのポインターであることを意味します。 この時点で、すでにxコマンドにポインターを再帰的に渡しているはずなので、何が起こったのか見てみましょう。



 (gdb) x/4xb a + 1 0x7fff5fbff570: 0x02 0x00 0x00 0x00
      
      







アドレス0x7fff5fbff570は0x7fff5fbff56c 、つまり配列aの最初のバイトのアドレスより正確に4つ多いことに注意してください。 int型がメモリ内で4バイトを占めることを考えると、 a + 1 は[1]を指していると結論付けることができます。



実際、Cの配列のインデックス付けは、ポインター演算の構文糖衣です。a [i]は *(a + i) 同等です。 GDBでこれを確認できます。



 (gdb) print a[0] $4 = 1 (gdb) print *(a + 0) $5 = 1 (gdb) print a[1] $6 = 2 (gdb) print *(a + 1) $7 = 2 (gdb) print a[2] $8 = 3 (gdb) print *(a + 2) $9 = 3
      
      





そのため、状況によっては配列のように振る舞うこともあれば、最初の要素へのポインターのように振る舞うこともあります。 何が起こっているの?



答えは次のとおりです。配列の名前がCの式で使用される場合、最初の要素へのポインタに「減衰」します。 このルールには2つの例外があります。配列名がsizeofに渡されるときと、配列名が演算子で使用されてアドレスを取得するときです。



演算子を使用するときに名前aが最初の要素へのポインターに分割されないという事実は、興味深い質問を提起します。aと&が分割されるポインターの違いは何ですか?



数値的には、両方とも同じアドレスを表します。



 (gdb) x/4xb a 0x7fff5fbff56c: 0x01 0x00 0x00 0x00 (gdb) x/4xb &a 0x7fff5fbff56c: 0x01 0x00 0x00 0x00
      
      





ただし、タイプは異なります。 すでに見たように、配列の名前は最初の要素へのポインタに分割されるため、 int *型でなければなりません。 type &aについては、GDBに問い合わせることができます。



 (gdb) ptype &a type = int (*)[3]
      
      





簡単に言えば、 は3つの整数の配列へのポインターです。 これは理にかなっています: 演算子に渡されたときにaは分割されず、aはint型です[3]



ポインタの算術に関してどのように動作するかの例で、ブレークダウンするポインタと操作の違いをトレースできます。



 (gdb) print a + 1 $10 = (int *) 0x7fff5fbff570 (gdb) print &a + 1 $11 = (int (*)[3]) 0x7fff5fbff578
      
      





1を追加するとアドレスが4単位増加し、1を&aに追加するとアドレスが12増加することに注意してください。



aが実際に分割するポインターの形式は&a [0]です。



 (gdb) print &a[0] $11 = (int *) 0x7fff5fbff56c
      
      





おわりに



GDBがCを探索するためのエレガントな研究環境であることを確信してほしい。printコマンドを使用して式の値を表示し、 xコマンドでメモリバイトを調べ、 ptypeコマンドを使用して型を操作できる。



GDBを使用したCの学習の実験を続ける予定の場合、いくつかの提案があります。



1. GDBを使用して、Ksplice Pointer Challengeに取り組みます。

2.構造がメモリに格納される方法を理解します。 それらは配列とどのように関係していますか?

3. GDB逆アセンブラーコマンドを使用して、アセンブラープログラミングを理解します。 関数呼び出しスタックがどのように機能するかを調べるのは特に楽しいです。

4.「TUI」GDBモードを確認します。これは、使い慣れたGDBを介したグラフィカルなncursesアドオンを提供します。 OS Xでは、おそらくソースからGDBをコンパイルする必要があります。



翻訳者から:伝統的に、LANを使用してエラーを示します。 私は建設的な批判を喜んでいます。



All Articles