Linux ARMのプログラムレジスタおよびスタックの初期状態
質問
現在、Linuxで学習演習としてARMアセンブリを使用しています。私は「裸の」アセンブリ、つまりlibcrtまたはlibgccを使用していません。誰もが、最初の命令が呼び出される前にプログラムの開始時にスタックポインタと他のレジスタがどのような状態になるかについての情報を教えてもらえますか?明らかにpc / r15は_startを指しており、残りは2つの例外を除いて0に初期化されているように見えます。 sp / r13は私のプログラムのはるか外側のアドレスを指し、r1は少し高いアドレスを指します。
いくつかの確かな質問:
- r1の値は何ですか?
- spの値はカーネルによって割り当てられた正当なスタックですか?
- そうでない場合、スタックを割り当てる好ましい方法は何ですか。 brkを使用するか、静的な.bssセクションを割り当てますか?
任意のポインタをいただければ幸いです。
解決
以下は、コンパイラでLinux / ARMプログラムを開始するために使用するものです。
/** The initial entry point.
*/
asm(
" .text\n"
" .globl _start\n"
" .align 2\n"
"_start:\n"
" sub lr, lr, lr\n" // Clear the link register.
" ldr r0, [sp]\n" // Get argc...
" add r1, sp, #4\n" // ... and argv ...
" add r2, r1, r0, LSL #2\n" // ... and compute environ.
" bl _estart\n" // Let's go!
" b .\n" // Never gets here.
" .size _start, .-_start\n"
);
ご覧のとおり、[sp]のスタックからargc、argv、およびenvironのものを取得しています。
若干の説明:スタックポインターは、プロセスのメモリ内の有効な領域を指します。 r0、r1、r2、およびr3は、呼び出される関数の最初の3つのパラメーターです。それぞれargc、argv、environを入力します。
他のヒント
これはLinuxなので、カーネルによってどのように実装されるかを見ることができます。
レジスタは、 start_thread
load_elf_binary
(最新のLinuxシステムを使用している場合、ほとんど常にELF形式を使用します)。 ARMの場合、レジスタは次のように設定されているようです:
r0 = first word in the stack
r1 = second word in the stack
r2 = third word in the stack
sp = address of the stack
pc = binary entry point
cpsr = endianess, thumb mode, and address limit set as needed
明らかに、有効なスタックがあります。 r0
- r2
の値はジャンクだと思うので、代わりにスタックからすべてを読む必要があります(この理由は後でわかります)。それでは、スタックの内容を見てみましょう。スタックから読み取るものは、 create_elf_tablesで埋められます。
。
ここで注目すべき興味深い点の1つは、この関数がアーキテクチャに依存しないことです。そのため、すべてのELFベースのLinuxアーキテクチャで同じものが(ほとんど)スタックに置かれます。以下は、あなたが読む順番にスタック上にあります:
- パラメーターの数(
main()
のargc
)。 - 各パラメーターのC文字列への1つのポインターと、それに続くゼロ(
main()
のargv
の内容、argv
は、これらのポインターの最初を指します)。 - 各環境変数のC文字列への1つのポインタと、それに続くゼロ(これは
main()
のまれに見られるenvp
3番目のパラメータの内容です。envp
は、これらのポインターの最初を指します)。 - 「補助ベクトル」。これはペアのシーケンス(タイプの後に値が続く)で、最初の要素がゼロ(
AT_NULL
)のペアで終了します。この補助ベクトルには、(glibcを使用している場合)LD_SHOW_AUXV
環境変数を1
に設定して動的にリンクされたプログラムを実行することにより、興味深い有用な情報が表示されます。 (たとえば、LD_SHOW_AUXV = 1 / bin / true
)。これは、アーキテクチャによって物事が少し異なる場合もあります。
この構造はどのアーキテクチャでも同じであるため、たとえば SYSV 386 ABI を使用して、物事がどのように適合するかをよりよく理解します(ただし、そのドキュメントの補助ベクトル型定数はLinuxが使用するものとは異なるため、それらのヘッダー)。
これで、 r0
- r2
の内容がゴミである理由を確認できます。スタックの最初の単語は argc
、2番目はプログラム名( argv [0]
)へのポインタ、3番目はおそらく引数なしのプログラム( argv [1]
)。古い a.out
バイナリ形式に対してこのように設定されていると思います。これは create_aout_tables
は、 argc
、 argv
、および envp
(したがって、これらは main()
r0 - r2
になります) >)。
最後に、なぜ r0
が1ではなく0だったのか(引数なしでプログラムを呼び出した場合、 argc
は1でなければならない)?私は、システムコールの戻り値(execが成功したためゼロになる)で、syscall機構の奥深くに何かを上書きしたと推測しています。 kernel_execve で確認できます。 code>
(これは
uClibc crt です。 r0
( atexit()
に登録される関数ポインターを含む)および sp
を含むすべてのレジスタが未定義であることを示唆しているようです有効なスタックアドレス。
したがって、 r1
に表示される値は、おそらく信頼できるものではありません。
一部のデータはスタックに配置されます。
ARM Linuxを使用したことはありませんが、libcrtのソースを調べてその動作を確認するか、gdbを使用して既存の実行可能ファイルにステップインすることをお勧めします。ソースコードは、アセンブリコードをステップ実行するだけでは必要ありません。
すべてのバイナリ実行可能ファイルによって実行される最初のコード内で、見つける必要があるすべてのことを行う必要があります。
これがお役に立てば幸いです。
トニー