【问题标题】:Return a value from procedure via the stack通过堆栈从过程中返回一个值
【发布时间】:2015-06-27 18:07:18
【问题描述】:

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

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

【问题讨论】:

  • 答案很大程度上取决于所使用的调用约定,了解这一点是件好事。以这种方式使用堆栈的函数具有非常特殊的调用约定,因此函数和调用者都应格外小心。

标签: assembly stack procedure


【解决方案1】:

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

.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 指定长度用户输入的字符串,以及字符串本身的第三个数据库。

Jester's 是该问题的另一种解决方案。甚至还有第三种解决方案:将数字右移(指令SHR),丢弃的位存储在进位标志中,然后我们可以通过指令JC或JNC检查进位标志是0还是1。

【讨论】:

  • 如果我通过堆栈向函数发送参数,
  • 当我在函数中写“ret”时,值不会变成“垃圾”吗?如果我通过堆栈向函数发送参数,通常我必须白色“ret”+参数数量 ident*2 。如果我想返回一个值,我应该将“pop”写入参数并弹出地址,并且而不是推送答案和地址?当我写“ret+ number”时会发生什么?参数会从堆栈中删除而答案不会吗?如果我不写pop到函数中的参数,只写地址,并推送答案和地址,我应该写“ret +num”吗?
  • 最好不要乱用“ret”。我个人不喜欢做“ret+num”之类的事情。您可以根据需要向堆栈添加任意数量的值,只需小心从“调用”弹出和推回返回地址。只要您照顾好退货地址,一切都会好起来的。这些值不会是“垃圾”,因为返回地址在它们的顶部,这就是为什么返回地址是我们压入堆栈的最后一件事,所以“ret”会首先弹出它。
  • 如果我有前任。 3个参数+答案+地址,我写“ret 6”,是删掉anwer+2个参数还是删掉3个参数?我的意思是,堆栈包含 5 个东西。当我写ret 3时,它们中的哪一个会被删除?地址下的 3 还是堆栈底部的 3?
  • 再次感谢您!它是如此清晰!并且你很耐心地回答!
【解决方案2】:

当函数返回栈上的值时,一般由

实现
  1. 删除调用序列中推送的所有值(堆叠 参数、帧指针和返回地址、保存的寄存器),生成相对于调用站点的干净堆栈,
  2. 将函数值推入干净的堆栈,
  3. 然后通过函数返回地址退出。

可以直接实现这些想法,也可以以优化的方式实现。

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

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 指令时,它假定原始返回地址是值(这就是它在影子堆栈中的内容)并且可以立即开始在返回点获取指令。 “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

【讨论】:

    【解决方案3】:

    这是一件特别愚蠢的事情,但这是可能的,当然。 如果您也获得堆栈上的输入,只需将其替换为结果即可。 假设是 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
    

    【讨论】:

    • 在堆栈上返回值有什么可笑的?高级语言编译器返回堆栈上的结构。至少,他们曾经如此。虽然在生产程序中返回堆栈上的寄存器大小的值可能很愚蠢,但这样做的技术与返回任意大的结构相同。这是一个很好的学习练习。
    • 我知道的约定使用指向返回值的内存区域的适用方法传递隐藏的 input 参数。值本身不必在堆栈上。弄乱调用者的堆栈帧不是一个好主意,特别是如果调用者释放了参数并且堆栈上有返回地址(例如 x86 stdcall)。
    猜你喜欢
    • 2011-07-18
    • 1970-01-01
    • 2017-09-06
    • 1970-01-01
    • 2014-09-27
    • 2014-03-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多