【问题标题】:Assembly program crashes on call or exit汇编程序在调用或退出时崩溃
【发布时间】:2019-01-27 23:45:44
【问题描述】:

在 VS2015 中调试我的代码,我到了程序的结尾。但是,call ExitProcess 或其任何变体上的寄存器应如此,会导致“访问冲突写入位置 0x00000004”。我正在使用 Kip Irvine 书中的 Irvine32.inc。我尝试过使用call DumpRegs,但这也会引发错误。

我尝试过使用call ExitProcess 的其他变体,例如exitinvoke ExitProcess,0,它们也不起作用,引发了同样的错误。之前,当我使用相同的格式时,代码运行良好。此代码与上一个代码之间的唯一区别是使用了通用寄存器。

include Irvine32.inc

.data
        ;ary        dword   100, -30, 25, 14, 35, -92, 82, 134, 193, 99, 0
        ary     dword   -24, 1, -5, 30, 35, 81, 94, 143, 0

.code
main PROC
                                ;ESI will be used for the array
                                ;EDI will be used for the array value
                                ;ESP will be used for the array counting
                                ;EAX will be used for the accumulating sum
                                ;EBX will be used for the average
                                ;ECX will be used for the remainder of avg
                                ;EBP will be used for calculating remaining sum
        mov     eax,0           ;Set EAX register to 0
        mov     ebx,0           ;Set EBX register to 0
        mov     esp,0           ;Set ESP register to 0
        mov     esi,OFFSET ary  ;Set ESI register to array
sum:    mov     edi,[esi]       ;Set value to array value
        cmp     edi,0           ;Check value to temination value 0
        je      finsum          ;If equal, jump to finsum
        add     esp,1           ;Add 1 to array count
        add     eax,edi         ;Add value to sum
        add     esi,4           ;Increment to next address in array
        jmp     sum             ;Loop back to sum array
finsum: mov     ebp,eax         ;Set remaining sum to the sum
        cmp     ebp,0           ;Compare rem sum to 0
        je      finavg          ;Jump to finavg if sum is 0
        cmp     ebp,esp         ;Check sum to array count
        jl      finavg          ;Jump to finavg if sum is less than array count
avg:    add     ebx,1           ;Add to average
        sub     ebp,esp         ;Subtract array count from rem sum
        cmp     ebp,esp         ;Compare rem sum to array count
        jge     avg             ;Jump to avg if rem sum is >= to ary count
finavg: mov     ecx,ebp         ;Set rem sum to remainder of avg

        call ExitProcess
main ENDP
END MAIN

call ExitProcess之前注册

EAX = 00000163 EBX = 0000002C ECX = 00000003 EDX = 00401055
ESI = 004068C0 EDI = 00000000 EIP = 0040366B ESP = 00000008
EBP = 00000003 EFL = 00000293 

OV = 0 UP = 0 EI = 1 PL = 1 ZR = 0 AC = 1 PE = 0 CY = 1 

【问题讨论】:

    标签: visual-studio assembly masm


    【解决方案1】:

    mov esp,0 将堆栈指针设置为 0。任何堆栈指令(如 push/pop 或 call/ret)在您执行此操作后都会崩溃。

    为您的临时数组计数选择一个不同的寄存器,而不是堆栈指针!您还有 7 个其他选择,看起来您还有未使用的 EDX。

    在正常的调用约定中,只有 EAX、ECX 和 EDX 被调用破坏(因此您可以在不保留调用者值的情况下使用它们)。但是您调用ExitProcess 而不是从main 返回,因此您可以销毁所有寄存器。但是ESP 必须在您call 时有效。

    call 通过将返回地址压入堆栈来工作,例如 sub esp,4 / mov [esp], next_instruction / jmp ExitProcess。见https://www.felixcloutier.com/x86/CALL.html。正如您的寄存器转储所示,call 之前的 ESP=8,这就是它尝试存储到绝对地址 4 的原因。


    您的代码有 2 个部分:循环遍历数组,然后找到平均值。 您可以在 2 个部分中将寄存器重复用于不同的事情,这通常会大大减少寄存器压力。 (即您不会用完寄存器。)

    在字符串之外使用隐式长度数组(由诸如0 之类的标记元素终止)是不寻常的。传递一个指针 + 长度的函数更为常见,而不仅仅是一个指针。

    但无论如何,您有一个隐式长度数组,因此您必须找到它的长度并在计算平均值时记住这一点。除了在循环内增加一个大小计数器,您可以从您也在增加的指针中计算它。 (或者使用计数器作为数组索引,如ary[ecx*4],但指针增量通常更有效。)

    以下是高效(标量)实现的样子。 (使用 SSE2 for SIMD,您可以通过一条指令添加 4 个元素...)

    总共只使用了 3 个寄存器。我本可以使用 ECX 而不是 ESI(所以 main 可以 ret 而不破坏调用者希望它保留的任何寄存器,只有 EAX、ECX 和 EDX),但我保留了 ESI 以与您的版本保持一致。

    .data
            ;ary        dword   100, -30, 25, 14, 35, -92, 82, 134, 193, 99, 0
            ary     dword   -24, 1, -5, 30, 35, 81, 94, 143, 0
    
    .code
    main PROC
    ;; inputs: static ary of signed dword integers
    ;; outputs: EAX = array average, EDX = remainder of sum/size
    ;;          ESI = array count (in elements)
    ;; clobbers: none (other than the outputs)
    
                                    ; EAX = sum accumulator
                                    ; ESI = array pointer
                                    ; EDX = array element temporary
    
            xor     eax, eax        ; sum = 0
            mov     esi, OFFSET ary ; incrementing a pointer is usually efficient, vs. ary[ecx*4] inside a loop or something.  So this is good.
    sumloop:                       ; do {
            mov     edx, [esi]
            add     edx, 4
            add     eax, edx        ; sum += *p++  without checking for 0, because + 0 is a no-op
    
            test    edx, edx        ; sets FLAGS the same as cmp edx,0
            jnz     sumloop         ; }while(array element != 0);
    ;;; fall through if the element is 0.
    ;;; esi points to one past the terminator, i.e. two past the last real element we want to count for the average
    
            sub     esi, OFFSET ary + 4  ; (end+4) - (start+4) = array size in bytes
            shr     esi, 2          ; esi = array length = (end-start)/element_size
    
            cdq                     ; sign-extend sum into EDX:EAX as an input for idiv
            idiv     esi            ; EAX = sum/length   EDX = sum%length
    
            call ExitProcess
    main ENDP
    

    我使用了 x86 的硬件除法指令,而不是减法循环。您的重复减法循环看起来非常复杂,但手动签名的除法可能很棘手。 我看不出您在哪里处理总和为负的可能性。如果您的数组的总和为负,重复减法会使其增长直到​​溢出。或者在您的情况下,如果 sum < count 则您将跳出循环,这将在第一次迭代时为负数。

    请注意,像 Set EAX register to 0 这样的 cmets 是没用的。通过阅读mov eax,0,我们已经知道这一点。 sum = 0 描述的是 语义 含义,而不是架构效果。有一些棘手的 x86 指令,在这种特定情况下评论它甚至做了什么是有意义的,但 mov 不是其中之一。

    如果您只是想在假设sum 一开始就非负的情况下进行重复减法,那么就这么简单:

    ;; UNSIGNED division  (or signed with non-negative dividend and positive divisor)
    ; Inputs: sum(dividend) in EAX,  count(divisor) in ECX
    ; Outputs: quotient in EDX, remainder in EAX  (reverse of the DIV instruction)
        xor    edx, edx                 ; quotient counter = 0
        cmp    eax, ecx
        jb     subloop_end              ; the quotient = 0 case
    repeat_subtraction:                 ; do {
        inc    edx                      ;   quotient++
        sub    eax, ecx                 ;   dividend -= divisor
        cmp    eax, ecx
        jae    repeat_subtraction       ; while( dividend >= divisor );
         ; fall through when eax < ecx (unsigned), leaving EAX = remainder
    subloop_end:
    

    注意在进入循环之前检查特殊情况如何让我们简化它。另见Why are loops always compiled into "do...while" style (tail jump)?

    sub eax, ecxcmp eax, ecx 在同一个循环中似乎是多余的:我们可以只使用 sub 设置标志,并纠正超调。

        xor    edx, edx                 ; quotient counter = 0
        cmp    eax, ecx
        jb     division_done            ; the quotient = 0 case
    repeat_subtraction:                 ; do {
        inc    edx                      ;   quotient++
        sub    eax, ecx                 ;   dividend -= divisor
        jnc    repeat_subtraction       ; while( dividend -= divisor doesn't wrap (carry) );
    
        add    eax, ecx                 ; correct for the overshoot
        dec    edx
    division_done:
    

    (但在大多数情况下,在大多数现代 x86 CPU 上这实际上并不快;即使输入不同,它们也可以并行运行 inc、cmp 和 sub。这可能对 AMD Bulldozer 有所帮助-整数核非常窄的系列。)

    显然,重复减法对于大数字的性能来说是完全垃圾。 可以实现更好的算法,例如一次一位长除法,但 idiv 指令除了您知道商为 0 或 1 的情况外,其他任何事情都会更快,因此最多需要 1 次减法。 (div/idiv 与任何其他整数运算相比都相当慢,但专用硬件比循环快得多。)

    如果确实需要手动实现有符号除法,一般是记录符号,取无符号绝对值,再进行无符号除法。

    例如xor eax, ecx / sets dl 如果 EAX 和 ECX 具有相同的符号,则为您 dl=0,如果它们不同,则为 1(因此商将为负数)。 (SF根据结果的符号位设置,异或不同输入为1,相同输入为0。)

    【讨论】:

    • 啊,我没有意识到这一点。将其更改为 EDX 后,它起作用了。我是组装新手,所以这很有帮助。谢谢!附带说明一下,我是否也应该像其他寄存器一样避免使用 EBP 和 EDI?
    • @CrazyJane:不,你应该使用所有 7 个通用寄存器,如果这样可以让你的代码更高效。 EBP 通常用作帧指针,但您可以将其用作暂存寄存器。您没有使用任何函数 args 或将任何本地变量从寄存器溢出到堆栈内存,因此帧指针不会为您带来任何好处,并且会花费额外的指令来设置。 IDK 为什么你提到 EDI,它并不比 ESI 或 EBX 更特别。
    • 我只是想确定一下,我是新人,而且对这门语言很熟悉。非常感谢您帮助我和提供信息。
    • @CrazyJane:您的代码的格式和注释都非常好,但对于初学者来说,通常有一些效率低下。 (这很正常,没有人用他们刚刚学习的语言编写出色的代码,尤其是 asm 语言)。您对所有内容都使用寄存器的本能绝对是好的,但是一旦您完成了对数组的循环,您就可以在计算平均值时重用一些临时寄存器。 (并且您可以在离开循环后从指针减法 + 右移获得长度,将指令保存在 inside 循环中。)
    • @CrazyJane:我很好奇你的循环的有效版本可以使用多少寄存器。我的只使用 3,请参阅我的编辑 :) 我注意到你的一个错误:如果 sum 为负数,你的重复减法循环看起来会失败。如果你不能只使用idiv(因为学校作业的一些限制?),你将不得不通过保存符号来解决这个问题,然后在绝对值的无符号除法之后应用它.
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-02-14
    • 2010-12-15
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多