7

宿題のために、いくつかのcファイルが与えられ、arm-linux-gccを使用してコンパイルしました(最終的にはgumstixボードを対象としますが、これらの演習ではqemuとemaを使用しています)。

質問の1つは、私を少し混乱させます-私たちは次のように言われています:

arm-linux-objdumpを使用して、実行可能バイナリのmain()で宣言された変数の場所を見つけます。

ただし、これらの変数はローカルであるため、実行時までアドレスを持つべきではありませんよね?

おそらく私が見つける必要があるのは、スタックフレームのオフセットであると考えています。これは、実際にはobjdumpを使用して見つけることができます(方法はわかりません)。

とにかく、この問題についての洞察をいただければ幸いです。必要に応じてソースコードを投稿させていただきます。

4

2 に答える 2

2
unsigned int one ( unsigned int, unsigned int );
unsigned int two ( unsigned int, unsigned int );
unsigned int myfun ( unsigned int x, unsigned int y, unsigned int z )
{
    unsigned int a,b;
    a=one(x,y);
    b=two(a,z);
    return(a+b);
}

コンパイルと逆アセンブル

arm-none-eabi-gcc -c fun.c -o fun.o
arm-none-eabi-objdump -D fun.o

コンパイラによって作成されたコード

00000000 <myfun>:
   0:   e92d4800    push    {fp, lr}
   4:   e28db004    add fp, sp, #4
   8:   e24dd018    sub sp, sp, #24
   c:   e50b0010    str r0, [fp, #-16]
  10:   e50b1014    str r1, [fp, #-20]
  14:   e50b2018    str r2, [fp, #-24]
  18:   e51b0010    ldr r0, [fp, #-16]
  1c:   e51b1014    ldr r1, [fp, #-20]
  20:   ebfffffe    bl  0 <one>
  24:   e50b0008    str r0, [fp, #-8]
  28:   e51b0008    ldr r0, [fp, #-8]
  2c:   e51b1018    ldr r1, [fp, #-24]
  30:   ebfffffe    bl  0 <two>
  34:   e50b000c    str r0, [fp, #-12]
  38:   e51b2008    ldr r2, [fp, #-8]
  3c:   e51b300c    ldr r3, [fp, #-12]
  40:   e0823003    add r3, r2, r3
  44:   e1a00003    mov r0, r3
  48:   e24bd004    sub sp, fp, #4
  4c:   e8bd4800    pop {fp, lr}
  50:   e12fff1e    bx  lr

簡単な答えは、メモリはコンパイル時と実行時の両方で「割り当てられる」ということです。コンパイル時にコンパイラがスタック フレームのサイズと誰がどこに行くかを決定するという意味でのコンパイル時。メモリ自体が動的なものであるスタック上にあるという意味でのランタイム。スタック フレームは、malloc() や free() とほぼ同じように、実行時にスタック メモリから取得されます。

呼び出し規則を知っておくと役立ちます。x は r0 に、y は r1 に、z は r2 に入ります。その場合、x は fp-16 に、y は fp-20 に、z は fp-24 にホームを持ちます。次に、one() の呼び出しには x と y が必要なので、スタック (x と y) からそれらをプルします。one() の結果は、a のホームである fp-8 に保存される a に入ります。等々。

関数 one は実際にはアドレス 0 ではありません。これは、リンクされたバイナリではなく、オブジェクト ファイルの逆アセンブリです。オブジェクトが残りのオブジェクトおよびライブラリとリンクされると、外部関数がある場所などの不足部分がリンカーによってパッチされ、one() および two() への呼び出しが実際のアドレスを取得します。(また、プログラムはアドレス 0 から開始されない可能性があります)。

私はここで少しごまかしましたが、コンパイラで最適化が有効になっておらず、このような比較的単純な関数がスタック フレームの理由がないことはわかっていました。

少しだけ最適化してコンパイルする

arm-none-eabi-gcc -O1 -c fun.c -o fun.o
arm-none-eabi-objdump -D fun.o

スタックフレームがなくなり、ローカル変数はレジスタに残ります。

00000000 : 0: e92d4038 push {r3, r4, r5, lr} 4: e1a05002 mov r5, r2 8: ebfffffe bl 0 c: e1a04000 mov r4, r0 10: e1a01005 mov r1, r5 14: ebfffffe bl 0 add 18: e0800004 r0、r0、r4 1c: e8bd4038 ポップ {r3、r4、r5、lr} 20: e12fff1e bx lr

コンパイラが代わりに行うことを決定したのは、スタックに保存することで、より多くのレジスタを使用できるようにすることです。なぜr3を救ったのかは謎ですが、それはまた別の話です...

関数 r0 = x、r1 = y、および r2 = z を呼び出し規則に従って入力すると、r0 と r1 をそのままにしておくことができます (one(y,x) でもう一度試して、何が起こるかを確認します)。再び使用されることはありません。呼び出し規約では、r0-r3 は関数によって破棄される可能性があるため、z を後で保持する必要があるため、r5 に保存する必要があります。one() の結果は、呼び出し規則ごとに r0 です。two() は r0-r3 を破壊する可能性があるため、後で使用するために a を保存する必要があります。two() の呼び出しの後、とにかく two の呼び出しにも r0 が必要なので、r4現在、a を保持しています。one を呼び出す前に z を r5 に保存しました (r2 では r5 に移動しました)。 z を保存した r5 を r1 に移動し、two() を呼び出します。呼び出し規約ごとの two() の結果。基本的な数学プロパティからの b + a = a + b であるため、返される前の最終的な加算は b + a である r0 + r4 であり、結果は規則に従って、関数から何かを返すために使用されるレジスタである r0 に入ります。スタックをクリーンアップし、変更されたレジスタを復元します。

myfun() は bl を使用して他の関数を呼び出したため、bl はリンク レジスタ (r14) を変更します。最終リターン (bx lr) であるため、lr はスタックにプッシュされます。規則では、関数内の r0 ~ r3 を破棄できますが、他のレジスタは破棄できないため、r4 と r5 は使用したためスタックにプッシュされます。なぜr3がスタックにプッシュされるのかは、呼び出し規約の観点からは必要ありません.64ビットメモリシステムを見越して行われたのだろうか.2回の完全な64ビット書き込みは、1回の64ビット書き込みと1回の32ビット書き込みよりも安価です. ただし、スタックの配置を知る必要があるため、これは単なる理論です。このコードで r3 を保持する理由はありません。

この知識を利用して、割り当てられたコードを逆アセンブルし (arm-...-objdump -D something.something)、同じ種類の分析を行います。特に、main() という名前の関数と、main という名前ではない関数 (意図的に main() を使用していません) では、スタック フレームのサイズが意味をなさないか、他の関数よりも意味をなさない可能性があります。上記の最適化されていないケースでは、合計 6 個の x、y、z、a、b とリンク レジスタ 6*4 = 24 バイトを格納する必要があり、結果としてサブ sp、sp、#24 になりました。スタック ポインターとフレーム ポインターの関係について少し説明します。フレームポインターを使用しないようにコンパイラーに指示するコマンドライン引数があると思います。-fomit-frame-pointer を使用すると、いくつかの命令が節約されます

00000000 <myfun>:
   0:   e52de004    push    {lr}        ; (str lr, [sp, #-4]!)
   4:   e24dd01c    sub sp, sp, #28
   8:   e58d000c    str r0, [sp, #12]
   c:   e58d1008    str r1, [sp, #8]
  10:   e58d2004    str r2, [sp, #4]
  14:   e59d000c    ldr r0, [sp, #12]
  18:   e59d1008    ldr r1, [sp, #8]
  1c:   ebfffffe    bl  0 <one>
  20:   e58d0014    str r0, [sp, #20]
  24:   e59d0014    ldr r0, [sp, #20]
  28:   e59d1004    ldr r1, [sp, #4]
  2c:   ebfffffe    bl  0 <two>
  30:   e58d0010    str r0, [sp, #16]
  34:   e59d2014    ldr r2, [sp, #20]
  38:   e59d3010    ldr r3, [sp, #16]
  3c:   e0823003    add r3, r2, r3
  40:   e1a00003    mov r0, r3
  44:   e28dd01c    add sp, sp, #28
  48:   e49de004    pop {lr}        ; (ldr lr, [sp], #4)
  4c:   e12fff1e    bx  lr

最適化すると、さらに多くの節約になります...

于 2013-03-03T18:20:21.210 に答える
1

それは、プログラムと、変数の場所をどの程度正確に知りたいかによって異なります。質問は、それらが格納されているコード セクションが必要ですか? .const .bss など? 特定のアドレスが必要ですか?いずれにせよ、良いスタートは objdump -S フラグを使用することです

objdump -S myprogram > dump.txt

これは、ソース コードとアドレスを含むアセンブリを混在させて出力するので便利です。ここから、あなたの検索を行うだけで、int main開始できます。

于 2013-03-03T01:34:15.743 に答える