アセンブリ:Y86スタックと呼び出し、pushl / poplおよびret命令
質問
間違ってコピーしない限り、上記のコードはクラスの黒板に、教師の助け/訂正で生徒によって書かれました:
int array[100], sum, i;
void ini() {
for(i = 0; i < 100; i++)
array[i] = i;
}
int main() {
ini();
sum = 0;
for(i = 0; i < 100; i++)
sum += array[i];
}
.pos 0
irmovl Stack, %esp
rrmovl Stack, %ebp
jmp main
array:
.pos 430
sum: .long 0
i: .long 0
main:
call ini //
irmovl <*>, %eax // %eax = 0
irmovl sum, %esi // %esi = 0xsum
rmmovl %eax, 0(%esi) // 0(%esi) = %eax <=> 0(0xsum) = 0 [sum = 0]
rmmovl %eax, 4(%esi) // 4(%esi) = %eax <=> 4(0xsum) = 0 [i = 0]
compare:
irmovl $100, %ebx // %ebx = 100
subl %eax, %ebx // %ebx = %ebx - %eax <=> %ebx = 100 - i
jle finish // Jumps to "finish" if SF=1 pr ZF=0
mrmovl 0(%esi), %edx // %edx = 0(%esi) <=> %edx = 0(0xsum) = sum
addl %eax, %edx // %edx = %edx + %eax <=> %edx = sum + i => sum
rmmovl %edx, 0($esi) // 0(%esi) = %edx <=> 0(0xsum) = sum
irmovl $1, %ecx // %ecx = 1
addl %ecx, %eax // %eax = %eax + %ecx <=> %eax = i + 1 => i
rmmovl %eax, 4(%esi) // 4($esi) = %eax <=> 4(0xsum) = i
jmp compare // Jumps unconditionally to "compare"
ini:
pushl %ebp //
rrmovl %esp, %ebp //
pushl %ebx //
pushl %eax //
irmovl <*>, %eax // %eax = 0
rmmovl %eax, -8(%ebp) //
ini_compare:
irmovl $100, %ecx // %ecx = 100
subl %eax, %ecx // %ecx = %ecx - %eax <=> %ecx = 100 - i
jle ini_finish // Jumps to "ini_finish" if SF=1 pr ZF=0
rrmovl %eax, %ebx // %ebx = %eax <=> %ebx = i
addl %eax, $ebx // %ebx = %ebx + %eax <=> %ebx = i + i = 2i
addl %ebx, %ebx // %ebx = %ebx + %ebx <=> %ecx = 2i + 2i = 4i
rmmovl %eax, array(%ebx) // array(%ebx) = %eax <=> array(0x4i) = i
irmovl %1, %ecx // %ecx = 1
addl %ecx, %eax // %eax = %eax + %ecx <=> %eax = i + 1 => i
rmmovl %eax, -8(%ebp) //
jmp ini_compare // Jumps unconditionally to "ini_compare"
ini_finish:
irmovl $4, %ebx //
addl %ebx, %esp //
popl %ebx //
popl %ebp //
ret //
.pos 600
Stack .long 0
ご覧のように、すべての指示にコメントがたくさんあり、それらのほとんどを取得しました(私は思います)。私はそれらをよく理解していません。また、スタックに何が起こっているのか、すべてのレコードがどこを指しているのかもわかりません。基本的に、何も書かれていないコメント(//)のある行。
このすべてがどのように機能するかを理解することは非常に重要です。うまくいけば、この混乱に光を当てることができる人もいます。
コメントに関するメモ:
- 0xsum:これは、アドレスが「合計」であることを意味するものではなく、不可能です。正確なメモリアドレスを使用せずに、私が話していることを理解するための手段にすぎません。
- [sum = 0]:これは、Cコードでは、変数sumがこの時点で0に設定されることを意味します。
- i + 1 =&gt; i:これは、「i」の値を1つ増やし、次の行では「i」が実際にその増分値を表すことを意味します。
解決
コードの一部を見てみましょう:
main:
call ini
これにより、命令ポインターの値がスタックにプッシュされ(後でコード内のこの位置に戻ることができます)、iniラベルのアドレスにジャンプします。 「ret」命令は、スタックに格納されている値を使用して、サブルーチンから戻ります。
以下はサブルーチンの初期化シーケンスです。スタック上のいくつかのレジスタの値を保存し、スタックポインタ(esp)をベースポインタレジスタ(ebp)にコピーしてスタックフレームを設定します。サブルーチンにローカル変数がある場合、スタックポインターはデクリメントされて、スタック上の変数のためのスペースが確保され、ベースポインターはスタックフレーム内のローカル変数にアクセスするために使用されます。この例では、唯一のローカル変数は(未使用の)戻り値です。
プッシュ命令は、プッシュされるデータサイズでスタックポインター(esp)をデクリメントし、そのアドレスに値を格納します。 pop命令は逆の動作を行い、最初に値を取得してからスタックポインターをインクリメントします。 (スタックが下方に大きくなるため、スタックが大きくなるとスタックポインタアドレスが低くなることに注意してください。)
ini:
pushl %ebp // save ebp on the stack
rrmovl %esp, %ebp // ebp = esp (create stack frame)
pushl %ebx // save ebx on the stack
pushl %eax // push eax on the stack (only to decrement stack pointer)
irmovl ini_finish:
irmovl $4, %ebx // ebx = 4
addl %ebx, %esp // esp += ebx (remove stack frame)
popl %ebx // restore ebx from stack
popl %ebp // restore ebp from stack
ret // get return address from stack and jump there
, %eax // eax = 0
rmmovl %eax, -8(%ebp) // store eax at ebp-8 (clear return value)
コードは標準パターンに従っているため、ローカル変数がなく、未使用の戻り値がある場合は少し厄介に見えます。ローカル変数がある場合、eaxをプッシュする代わりに、減算を使用してスタックポインターをデクリメントします。
以下はサブルーチンの終了シーケンスです。スタックフレームが作成される前の位置にスタックを復元し、サブルーチンを呼び出したコードに戻ります。
<*>コメントへの応答:
値を保持するために、ebxレジスタがプッシュおよびポップされます。おそらく、このコードではなく、レジスタが非常に一般的に使用されているため、コンパイラは常にこのコードを常にそこに配置します。同様に、スタックフレームは、本当に必要ではない場合でも、espをebpにコピーすることで常に作成されます。
eaxをプッシュする命令は、スタックポインタをデクリメントするためだけにあります。スタックポインターを減算するよりも短くて速いので、小さなデクリメントに対してそのように行われます。予約されているスペースは戻り値用です。ここでも、戻り値が使用されていない場合でも、コンパイラーは常にこれを行うようです。
図では、espレジスタは常にメモリの4バイトが高すぎることを示しています。スタックポインターは値をプッシュした後にデクリメントされるため、次の値ではなくプッシュされた値を指すことに注意してください。 (メモリアドレスもかなり離れています。0x20ではなく0x600のようなものです。スタックラベルが宣言されている場所です。)