这种技术和高级语言编译器的工作原理密切相关。我们下面结合 C 语言的函数调用,看一下用栈传递参数的思想。
用栈传递参数的原理十分简单,就是由调用者将需要传递给子程序的参数压入栈中,子程序从栈中取得参数。我们看下面的例子。
;说明:计算( a – b ) ^ 3 ,a、b 为 word 型数据。
;参数:进入子程序时,栈顶存放IP,后面依次存放 a、b
;结果:( dx : ax ) = ( a – b ) ^ 3 ( 乘法,两个数都是8位,结果存放在AX中;如果两个数都是16位,高位存放在DX中,低位存放在AX中 )
difcube :
push bp
mov bp , sp
mov ax , [ bp + 4 ] ; 将栈中a的值送入ax 中
sub ax , [ bp + 6 ] ; 减栈中b的值
mov bp , ax
mul bp
mul bp
pop bp
ret 4 ; call 返回,并恢复sp
指令 ret n 的含义用汇编语法描述为:
pop ip
add sp , n
因为用栈传递参数,所以调用者在调用程序的时候要向栈中压入参数,子程序在返回的时候可以用ret n 指令将栈顶指针修改为调用前的值。调用上面的子程序之前,需要压入两个参数。所以用ret 4 返回。
我们看一下如何调用上面的程序,设 a = 3 、b = 1 ,下面的程序段计算:( a – b ) ^ 3:
mov ax , 1
push ax
mov ax , 3
push ax ;注意参数压栈的顺序
call difcube
|
我们看一下程序的执行过程中栈的变化:
(1)假设栈的初始情况如下:
1000:0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
↑ss:sp
(2)执行以下指令:
mov ax , 1
push ax
mov ax , 3
push ax
栈的情况变为:
a b
1000:0 00 00 00 00 00 00 00 00 00 00 00 00 03 00 01 00
↑ss:sp
(3)执行指令: call difcube
栈的情况变为:
IP a b
1000:0 00 00 00 00 00 00 00 00 00 00 XX XX 03 00 01 00
↑ss:sp
(4)执行指令: push bp
栈的情况变为:
bp IP a b
1000:0 00 00 00 00 00 00 00 00 XX XX XX XX 03 00 01 00
↑ss:sp
(5)执行指令: mov bp , sp ;ss:bp 指向 1000 : 8
bp IP a b
1000:0 00 00 00 00 00 00 00 00 XX XX XX XX 03 00 01 00
↑ss:sp(ss:bp)
(6)执行以下指令:
mov ax , [ bp + 4 ] ; 将栈中a的值送入ax 中
sub ax , [ bp + 6 ] ; 减栈中b的值
mov bp , ax
mul bp
mul bp ; DX:AX 中现在存放的是最终结果
(7)执行指令: pop bp
栈的情况变为:
IP a b
1000:0 00 00 00 00 00 00 00 00 XX XX XX XX 03 00 01 00
↑ss:sp
(8)执行指令: ret 4
栈的情况变为:
1000:0 00 00 00 00 00 00 00 00 XX XX XX XX 03 00 01 00
↑ss:sp
|
assume cs:code
code segment
start:
mov ax , 1
push ax
mov ax , 3
push ax
call difcube
mov ax , 4c00h
int 21h
difcube:
push bp
mov bp , sp
mov ax , [bp+4]
sub ax , [bp+6]
mov bp , ax
mul bp
mul bp
pop bp
ret 4
code ends
end start
|
// 最开始SS:SP=0769:0 因为入栈后sp-2=FFFE // 这段空间才是真正的栈空间 // 1和3入栈 // call执行,IP,BP入栈 // BP出栈 // ret返回,sp恢复原来的0,结果=8,DX:AX=0000:0008 |
下面我们通过一个C语言程序编译后的汇编语言程序,看一下栈在参数传递中的应用。要注意的是,在C语言中,局部变量也在栈中存储。
C程序
void add ( int , int , int ) ;
main ( )
{
int a = 1 ;
int b = 2 ;
int c = 0 ;
add ( a , b , c ) ;
c++ ;
}
void add ( int a , int b , int c ){
c = a + b ;
}
|
编译后的汇编程序
assume cs:code
code segment
start:
mov bp , sp
sub sp , 6
mov word ptr [bp-6] , 0001 ; int a
mov word ptr [bp-4] , 0002 ; int b
mov word ptr [bp-2] , 0000 ; int c
push [bp-2]
push [bp-4]
push [bp-6]
call addr
add sp ,6
inc word ptr [bp-2]
addr:
push bp
mov bp , sp ; bp 暂存刚入栈时候的sp,防止后面一些操作会改变sp,从而获取不到要得到的值;还有一种原因是先分配空间,sp就不改变了,只能通过bp来对空间再赋值
mov ax , [bp+4] ; a
add ax , [bp+6] ; b
mov [bp+8] , ax ; c
mov sp , bp ; 恢复入栈 sp
pop bp
ret ; 子程序返回,pop ip
code ends
end start
// 加上了宏汇编开头两句和最后两句什么的,便于编译然后debug
|
// 初始;SS:SP=0769:0000 0769:FFF0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
↑ss:sp
// 进入main,定义变量a、b、c,开辟变量空间
0769:FFF0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
↑ss:sp ↑ss:bp
// 开辟好空间,赋值
a b c
0769:FFF0 00 00 00 00 00 00 00 00 00 00 10 00 20 00 00 00
↑ss:sp ↑ss:bp
画的所有情况都是理想下的sp不更改的情况,但是实际上有些操作会修改sp,所以要用bp来保存初始的sp,然后通过bp加减法得到栈中想要访问的值 // a、b、c 进栈
// 再次push a、b、c
// IP进栈 IP=0020 , bp 进栈 bp=0000
// mov [bp+8] , ax ; c=3
// pop bp,bp=0000
// ret 返回,pop IP
// c++,c=1
|
// 进入子函数add(a , b , c),子函数要为a、b、c临时分配空间 a b c a b c
0769:FFF0 00 00 00 00 10 00 20 00 00 00 10 00 20 00 00 00
↑ss:sp ↑ss:bp
// 汇编call进入子程序,IP 进栈 IP a b c a b c
0769:FFF0 00 00 20 00 10 00 20 00 00 00 10 00 20 00 00 00
↑ss:sp ↑ss:bp
// bp 进栈 bp IP a b c a b c
0769:FFF0 00 00 20 00 10 00 20 00 00 00 10 00 20 00 00 00
↑ss:sp ↑ss:bp
bp IP a b c a b c
0769:FFF0 00 00 20 00 10 00 20 00 00 00 10 00 20 00 00 00
↑ss:sp(bp)
// a+b=c,c=3
bp IP a b c a b c
0769:FFF0 00 00 20 00 10 00 20 00 30 00 10 00 20 00 00 00
↑ss:sp(bp)
// pop bp,ret
a b c a b c
0769:FFF0 00 00 20 00 10 00 20 00 30 00 10 00 20 00 00 00
↑ss:sp (bp)=0
// sp+6,退出了子函数,释放空间
a b c
0769:FFF0 00 00 20 00 10 00 20 00 30 00 10 00 20 00 00 00
↑ss:sp (bp)=0
到这里子函数add(a , b , c)开辟的空间被释放
// inc word ptr [bp-2]
a b c
0769:FFF0 00 00 20 00 10 00 20 00 30 00 10 00 20 00 01 00
↑ss:sp (bp)=0
// main函数结束,恢复sp=bp,释放所有开辟的空间
0769:FFF0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
↑ss:sp
|