画面に正方形を描画するサブプログラムをアセンブリで作成しようとしています。 C++の場合のようにサブプログラムにパラメーターを渡すことができないと思うので、スタックを使用してパラメーターを格納およびアクセスできると考えました(変数が多すぎるため、共通のデータレジスタを使用できません。パス)。
問題は(どこかで読んだことを覚えています)、現在の「プログラム」のアドレスに対してcallコマンドを使用するとスタックに保存されるため、「ret」コマンドを使用するとどこに戻るかがわかります。しかし、スタックに何かを格納してから関数を呼び出すと、アドレスのどこかに(つまり、スタックの一番上に)保存してから、パラメーターを安全にポップする必要があります。次に、コードが終了した後、「ret」を呼び出す前に、アドレスをプッシュバックする必要があります。
私は正しいですか?また、「はい」の場合、アドレスはどこに格納できますか(AX、BX、またはその他のデータレジスタに収まるように、アドレスの長さが1バイトだけではないと思います)。 IPを使用してこれを行うことはできますか(これが他の目的で使用されていることはわかっていますが)?
これは私が想像するものです:
[BITS 16]
....
main:
mov ax,100b
Push ax
call rectangle ;??--pushes on the stack the current address?
jml $
rectangle:
pop ax ;??--this is the addres of main right(where the call was made)?
pop bx ;??--this is the real 100b, right?
....
Push ax
ret ;-uses the address saved in stack
通常、パラメータとローカルを参照するには、ベースポインタ(16ビットではbp
、32ビットではebp
)を使用します。
基本的な考え方は、関数に入るたびに、スタックポインターをベースポインター内に保存し、関数の実行中に関数が「固定参照ポイント」として呼び出されたときにスタックポインターを保持することです。このスキーマでは[ebp-something]
通常はローカル、[ebp+something]
はパラメータです。
次のように実行できる、一般的な32ビットの呼び出し先クリーンアップ呼び出し規約を置き換えます。
発信者:
Push param1
Push param2
call subroutine
サブルーチン:
Push bp ; save old base pointer
mov bp,sp ; use the current stack pointer as new base pointer
; now the situation of the stack is
; bp+0 => old base pointer
; bp+2 => return address
; bp+4 => param2
; bp+6 => param1
mov ax,[bp+4] ; that's param2
mov bx,[bp+6] ; that's param1
; ... do your stuff, use the stack all you want,
; just make sure that by when we get here Push/pop have balanced out
pop bp ; restore old base pointer
ret 4 ; return, popping the extra 4 bytes of the arguments in the process
これは、呼び出し元の観点から、関数がsp
を変更することを除いて機能します。 32ビットのほとんどの呼び出し規約では、関数はeax/ecx/edx
の変更のみが許可されており、他のregを使用する場合は、それらを保存/復元する必要があります。 16ビットも似ていると思います。 (もちろん、asmでは、任意のカスタム呼び出し規約を使用して関数を記述できます。)
一部の呼び出し規約では、呼び出し元が呼び出し元によってプッシュされた引数をポップすることを想定しているため、この場合は実際に機能します。 Matteoの答えのret 4
はそれを行います。 (呼び出し規約やその他の優れたリンクについては、 x86 タグwikiを参照してください。)
それは非常に奇妙で、物事を行うための最良の方法ではありません。そのため、通常は使用されません。 最大の問題は、ランダムアクセスではなく、パラメータへのアクセスのみを順番に提供することです。ポップするレジスタが不足しているため、最初の6個程度の引数にしかアクセスできません。
また、リターンアドレスを保持するレジスタを拘束します。 x86(x86-64より前)にはレジスタがほとんどないため、これは本当に悪いことです。他の関数引数をレジスタにポップした後、リターンアドレスをプッシュして、使用できるように解放することができます。
jmp ax
は技術的にはPush
/ret
の代わりに機能しますが、これはreturn-address予測子を無効にし、将来のret
命令を遅くします。
しかしとにかく、Push bp
/mov bp, sp
でスタックフレームを作成することは、安価でスタックへのランダムアクセスを提供するため、16ビットコードで広く使用されています。 ([sp +/- constant]
は16ビットでは有効なアドレッシングモードではありません(ただし、32ビットと64ビットでは有効です)。([bp +/- constant]
は有効です)。その後、必要なときにいつでもそれらから再ロードできます。
32ビットおよび64ビットコードでは、コンパイラは、命令を無駄にしてebp
を拘束する代わりに、[esp + 8]
などのアドレッシングモードを使用するのが一般的です。 (-fomit-frame-pointer
がデフォルトです)。つまり、esp
への変更を追跡して、異なる命令で同じデータの正しいオフセットを計算する必要があるため、手書きのasm、特にチュートリアルや教材では一般的ではありません。実際のコードでは、効率を犠牲にする場合はCコンパイラを使用するだけなので、明らかに最も効率的な方法を実行します。
C++のようにサブプログラムにパラメータを渡すことはできないと思います[...]
以下の例に示すように、パラメーターをサブルーチンに渡すには、次のトリックを実行できます。
.486
assume cs:code, ds:data, ss:stack
macro_for_subroutine macro parameter1, parameter2
Push parameter1 ; [bp+6]
Push parameter2 ; [bp+4]
call subroutine ; [bp+2] (return address pushed onto the stack)
endm
stack segment use16 para stack
db 256 dup(' ')
stack ends
data segment use16
value1 dw 0
value2 dw 0
data ends
code segment use16 para public 'code'
start:
main proc far
; set up stack for return
Push ds
mov ax, 0
Push ax
; ----
; set DS register to data segment
mov ax, data
mov ds, ax
macro_for_subroutine 1111h, 2222h
ret ; return to DOS
main endp
subroutine proc near
Push bp ; [bp+0]
mov bp, sp
Push ax
Push bx
mov ax, [bp+6] ; parameter1
mov value1, ax
mov bx, [bp+4] ; parameter2
mov value2, bx
pop bx
pop ax
pop bp
ret 4 ; return and then increase SP by 4, because we
; pushed 2 parameters onto the stack from the macro
subroutine endp
code ends
end start
注:これは16ビットのMASMDOSアセンブリで記述されています。
マクロはパラメーターを受け入れることができます。したがって、特定のサブルーチンのマクロを定義することにより、パラメーターを使用してサブルーチンの呼び出しをシミュレートできます。マクロ内で、パラメーターを目的の順序でスタックにプッシュしてから、サブルーチンを呼び出します。
文字列変数を渡すことはできませんが、オフセットを渡すことはできます(詳細については、 x86アセンブリ-masm32:変数をスタックにプッシュする際の問題 を参照してください)。