Assembly 通过堆栈从过程返回一个值

Assembly 通过堆栈从过程返回一个值,assembly,stack,procedure,Assembly,Stack,Procedure,我正在学习汇编,我必须编写一个程序(函数),它获取一个数字,如果是偶数,则返回1,如果不是,则返回0 我必须返回答案,而不是通过寄存器或标志,而是通过堆栈(例如,我不能将答案放入bx或ax并在主程序中检查它们的值)。我该怎么做呢?这是一件特别愚蠢的事情,但这当然是可能的。 如果您也在堆栈上获得了输入,只需将其替换为结果即可。 假设是16位代码,类似这样: push bp mov bp, sp and word [bp+4], 1 ; keep lowest bit xor byte [bp+4]

我正在学习汇编,我必须编写一个程序(函数),它获取一个数字,如果是偶数,则返回
1
,如果不是,则返回
0


我必须返回答案,而不是通过寄存器或标志,而是通过堆栈(例如,我不能将答案放入
bx
ax
并在主程序中检查它们的值)。我该怎么做呢?

这是一件特别愚蠢的事情,但这当然是可能的。 如果您也在堆栈上获得了输入,只需将其替换为结果即可。 假设是16位代码,类似这样:

push bp
mov bp, sp
and word [bp+4], 1 ; keep lowest bit
xor byte [bp+4], 1 ; flip it to return 1 for even
pop bp
ret

下一个程序是用EMU8086 Intel语法编写的(只需复制、粘贴和运行),它就是这样做的:显示消息,从键盘捕获数字,将数字从字符串转换为数字,检查数字是偶数还是奇数,在堆栈中存储“1”或“0”,并根据“1”或“0”显示消息。这里有大量的评论可以帮助您理解:

.stack 100h
;------------------------------------------
.data
msj1  db 13,10,'Enter the number: $'
msj2  db 13,10,'The number is even$'
msj3  db 13,10,'The number is odd$'
str   db 6 ;MAX NUMBER OF CHARACTERS ALLOWED (5).
      db ? ;LENGTH (NUMBER OF CHARACTERS ENTERED BY USER).
      db 6 dup (?) ;CHARACTERS ENTERED BY USER. 
;------------------------------------------
.code          
;INITIALIZE DATA SEGMENT.
  mov  ax, @data
  mov  ds, ax

;DISPLAY MESSAGE.
  call clear_screen  ;DECLARED AT THE END OF THIS CODE.
  mov  ah, 9
  mov  dx, offset msj1
  int  21h

;CAPTURE NUMBER FROM KEYBOARD AS STRING.
  mov  ah, 0Ah
  mov  dx, offset str
  int  21h

;CONVERT CAPTURED NUMBER FROM STRING TO NUMERIC.
  mov  si, offset str ;PARAMETER FOR STRING2NUMBER.
  call string2number ;NUMBER RETURNS IN BX. 

;CALL PROC TO FIND OUT IF NUMBER IS EVEN OR ODD. THE INSTRUCTION
;"CALL" WILL PUSH IN STACK THE ADDRESS OF THIS INSTRUCTION, THAT'S 
;HOW IT KNOWS HOW TO COME BACK HERE TO CONTINUE EXECUTION.
  call even_or_odd

;GET RESULT FROM STACK.
  pop  ax  

;DISPLAY RESULT.
  cmp  al, '1'
  je   even_number

;IF NO JUMP, AL == '0'.  
  mov  ah, 9
  mov  dx, offset msj3
  int  21h           
  jmp  wait_for_key  ;SKIP "EVEN_NUMBER".
even_number:  
  mov  ah, 9
  mov  dx, offset msj2
  int  21h           

;WAIT FOR USER TO PRESS ANY KEY.
wait_for_key:
  mov  ah,7
  int  21h

;FINISH THE PROGRAM.
  mov  ax, 4c00h
  int  21h           

;------------------------------------------
;THIS PROCEDURE RETURNS '1' IN STACK IF THE NUMBER
;IS EVEN OR '0' IF IT'S ODD.
;ASSUME THE NUMBER COMES IN BX.

proc even_or_odd
;DIVIDE NUMBER BY 2.    
  mov  ax, bx
  mov  bl, 2
  div  bl  ;AX / BL (NUMBER / 2). RESULT : QUOTIENT=AL, REMAINDER=AH.

;IF REMAINDER IS 0 THEN NUMBER IS EVEN, ELSE IT'S ODD.
  cmp  ah, 0
  je   its_even

;IF NO JUMP, IT'S ODD.
  mov  ax, '0'  ;VALUE TO STORE IN STACK.
  jmp  finish   ;SKIP "ITS_EVEN".
its_even:  
  mov  ax, '1'  ;VALUE TO STORE IN STACK.
finish:

;STORE VALUE IN STACK. IMPORTANT: WHEN THIS PROCEDURE
;WAS CALLED, THE RETURN ADDRESS WAS PUSHED IN STACK. TO
;RETURN THE VALUE IN STACK IT'S NECESSARY TO RETRIEVE
;THE RETURN ADDRESS FIRST, PUSH THE VALUE ('0' OR '1')
;AND PUSH THE RETURN ADDRESS BACK.
  pop  bx  ;RETRIEVE RETURN ADDRESS FROM THE CALL.
  push ax  ;VALUE TO RETURN ('0' OR '1').
  push bx  ;PUT RETURN ADDRESS BACK.

  ret  ;THIS "RET" POPS THE RETURN ADDRESS. THIS IS HOW
endp   ;IT KNOWS HOW TO RETURN WHERE THE PROC WAS CALLED.  

;------------------------------------------
;CONVERT STRING TO NUMBER IN BX.
;SI MUST ENTER POINTING TO THE STRING.

proc string2number
;MAKE SI TO POINT TO THE LEAST SIGNIFICANT DIGIT.
  inc  si ;POINTS TO THE NUMBER OF CHARACTERS ENTERED.
  mov  cl, [ si ] ;NUMBER OF CHARACTERS ENTERED.                                         
  mov  ch, 0 ;CLEAR CH, NOW CX==CL.
  add  si, cx ;NOW SI POINTS TO LEAST SIGNIFICANT DIGIT.

;CONVERT STRING.
  mov  bx, 0
  mov  bp, 1 ;MULTIPLE OF 10 TO MULTIPLY EVERY DIGIT.
repeat:
;CONVERT CHARACTER.                    
  mov  al, [ si ] ;CHARACTER TO PROCESS.
  sub  al, 48 ;CONVERT ASCII CHARACTER TO DIGIT.
  mov  ah, 0 ;CLEAR AH, NOW AX==AL.
  mul  bp ;AX*BP = DX:AX.
  add  bx,ax ;ADD RESULT TO BX. 
;INCREASE MULTIPLE OF 10 (1, 10, 100...).
  mov  ax, bp
  mov  bp, 10
  mul  bp ;AX*10 = DX:AX.
  mov  bp, ax ;NEW MULTIPLE OF 10.  
;CHECK IF WE HAVE FINISHED.
  dec  si ;NEXT DIGIT TO PROCESS.
  loop repeat ;COUNTER CX-1, IF NOT ZERO, REPEAT.

  ret 
endp    

;------------------------------------------
proc clear_screen
  mov  ah,0
  mov  al,3
  int  10H
  ret
endp
请注意,用于从键盘捕获数字的变量“str”使用3-DB格式:第一个DB指定最大长度(加上一个表示结束chr(13)),另一个DB表示用户输入的字符串长度,第三个DB表示字符串本身


杰斯特是解决这个问题的另一个办法。甚至还有第三种解决方案:将数字向右移动(指令SHR),删除的位存储在进位标志中,然后我们可以用指令JC或JNC检查进位标志是0还是1。

当函数在堆栈上返回值时,通常由

  • 删除调用序列中推送的所有值(堆叠 参数、帧指针和返回地址、保存的寄存器),生成关于调用站点的干净堆栈
  • 将函数值推送到干净的堆栈上
  • 然后通过函数返回地址退出
  • 一个人可以直接实现这些想法,或者以优化的方式实现它们

    下面是执行此操作的典型、简单的代码:

    call_site:  call get_number  ; assumed to eax
                 push eax         ;  push argument onto the stack
                 call is_even_or_odd
                 pop  eax         ; get the function result back from the stack
                 test eax, eax
                 je   even
     odd:        ...
    
    
     is_even_or_odd:
                 push   ebp        ; save frame pointer
                 mov    eax, 8[ebp] ; get argument (above saved EBP and return address)
                 and    eax, 1      ; now eax == 0 if even, 1 if odd
    
                 pop    ebp         ; pop the push values from the stack
                 pop    edx
                 leas   4[esp]      ; pop the argument
                 push   eax         ; push the result
                 jmp    edx         ; go to the return address
    
    上述例行程序以一般方式编码。此特定例程可以更紧凑地编码,并具有更好的性能:

     is_even_or_odd:
                 ; no need to save frame pointer; just leave EBP alone
                 pop    edx         ; get return address
                 pop    eax         ; pop the argument
                 and    eax, 1      ; now eax == 0 if even, 1 if odd
                 push   eax         ; push the result
                 push   edx         ; instead of "jmp edx"
                 ret
    
    最后,推送返回地址,然后执行“ret”这一奇特的习惯用法让硬件在其影子堆栈中精确跟踪返回地址。这意味着当它点击ret指令时,它假定原始返回地址是value(它在卷影堆栈中的值),并且可以立即在返回点开始获取指令。“jmp-edx”习惯用法可以工作,但会中断分支地址预测,从而减慢从子例程返回所需的时间

    还有一个变体使用调用堆栈中的空间作为参数, 返回结果。当参数的大小, 等于结果的大小,如本例所示:

     is_even_or_odd:
                 ; no need to save frame pointer; just leave EBP alone
                 mov    eax, 4[esp] ; get the argument
                 and    eax, 1      ; now eax == 0 if even, 1 if odd
                 mov    4[esp], eax ; smash the argument with the result
                 ret
    

    答案在很大程度上取决于所使用的调用约定,了解这一点是件好事。以这种方式使用堆栈的函数有一个非常特殊的调用约定,因此编写函数和调用方时应该格外小心。在堆栈上返回值有什么傻的?高级语言编译器返回堆栈上的结构。至少,他们过去是这样。虽然在生产程序中,在堆栈上返回寄存器大小的值可能很愚蠢,但这样做的技术与返回任意大的结构的技术相同。这是一个很好的学习练习。我知道的约定使用适用的方法传递一个隐藏的输入参数,该方法指向返回值的内存区域。值本身不必在堆栈上。弄乱调用方的堆栈帧不是一个好主意,特别是如果调用方释放了参数,并且堆栈上有一个返回地址(例如x86 stdcall)。如果我通过堆栈向函数发送参数,当我在函数的中写入“ret”时,值不是变成了“垃圾”吗?如果我通过堆栈向函数发送参数,通常我必须将“ret”加上参数ident*2的数字置为白色。如果我想返回一个值,我是否应该将“pop”写入参数并弹出地址,而不是按下答案和地址?当我写“ret+number”时会发生什么?参数是否会从堆栈中删除,而不会从anwer中删除?如果我不将pop写入函数中的参数,只写入地址,并按下答案和地址,我是否应该写入“ret+num”?最好不要弄乱“ret”。我个人不喜欢做像“ret+num”这样的事情。您可以根据需要向堆栈中添加任意多的值,只需小心弹出并从“调用”返回返回地址即可。只要你注意回邮地址,一切都会好的。这些值不会是“垃圾”,因为返回地址在它们上面,这就是为什么返回地址是我们在堆栈上推的最后一个东西,所以“ret”会首先弹出它。如果我有例如3个参数+答案+地址,我写“ret 6”,anwer+2参数会被删除还是3个参数会被删除?我的意思是,这个堆栈包含5个东西。当我写ret 3时,哪一个会被删除?地址下面的3个还是堆栈底部的3个?再次-谢谢!太清楚了!你是如此耐心地回答!