汇编程序在调用或退出时崩溃
Assembly program crashes on call or exit
在 VS2015 中调试我的代码,我到了程序的末尾。寄存器是它们应该的,但是,在 call ExitProcess
或它的任何变体上,会导致 "Access violation writing location 0x00000004." 我正在使用 Kip Irvine 书中的 Irvine32.inc。我试过使用 call DumpRegs
,但这也会引发错误。
我试过使用 call ExitProcess
的其他变体,例如 exit
和 invoke 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
mov esp,0
将堆栈指针设置为 0。任何像 push/pop 或 call/ret 这样的堆栈指令都会在你这样做后崩溃。
为您的数组计数临时选择一个不同的寄存器,而不是堆栈指针!您还有 7 个其他选择,看起来您还有未使用的 EDX。
在正常的调用约定中,只有 EAX、ECX 和 EDX 被调用破坏(因此您可以在不保留调用者值的情况下使用它们)。但是您是从 main
调用 ExitProcess
而不是 return,因此您可以销毁所有寄存器。但是当你 call
.
时 ESP
必须有效
call
通过将 return 地址压入堆栈来工作,例如 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]
,但指针递增通常更有效。)
下面是高效(标量)实现的样子。 (使用用于 SIMD 的 SSE2,您可以使用一条指令添加 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
之类的注释是无用的。我们已经通过阅读 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:
请注意如何在进入循环之前检查特殊情况让我们简化它。另见
sub eax, ecx
和 cmp 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
与任何其他整数运算相比相当慢,但专用硬件比循环快得多。)
如果你确实需要手动实现有符号除法,通常你记录符号,取无符号绝对值,然后做无符号除法。
例如如果 EAX 和 ECX 具有相同的符号,xor eax, ecx
/ sets dl
给你 dl=0,或者如果它们不同则为 1(因此商将为负)。 (SF根据结果的符号位设置,XOR不同输入取1,相同输入取0。)
在 VS2015 中调试我的代码,我到了程序的末尾。寄存器是它们应该的,但是,在 call ExitProcess
或它的任何变体上,会导致 "Access violation writing location 0x00000004." 我正在使用 Kip Irvine 书中的 Irvine32.inc。我试过使用 call DumpRegs
,但这也会引发错误。
我试过使用 call ExitProcess
的其他变体,例如 exit
和 invoke 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
mov esp,0
将堆栈指针设置为 0。任何像 push/pop 或 call/ret 这样的堆栈指令都会在你这样做后崩溃。
为您的数组计数临时选择一个不同的寄存器,而不是堆栈指针!您还有 7 个其他选择,看起来您还有未使用的 EDX。
在正常的调用约定中,只有 EAX、ECX 和 EDX 被调用破坏(因此您可以在不保留调用者值的情况下使用它们)。但是您是从 main
调用 ExitProcess
而不是 return,因此您可以销毁所有寄存器。但是当你 call
.
ESP
必须有效
call
通过将 return 地址压入堆栈来工作,例如 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]
,但指针递增通常更有效。)
下面是高效(标量)实现的样子。 (使用用于 SIMD 的 SSE2,您可以使用一条指令添加 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
之类的注释是无用的。我们已经通过阅读 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:
请注意如何在进入循环之前检查特殊情况让我们简化它。另见
sub eax, ecx
和 cmp 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
与任何其他整数运算相比相当慢,但专用硬件比循环快得多。)
如果你确实需要手动实现有符号除法,通常你记录符号,取无符号绝对值,然后做无符号除法。
例如如果 EAX 和 ECX 具有相同的符号,xor eax, ecx
/ sets dl
给你 dl=0,或者如果它们不同则为 1(因此商将为负)。 (SF根据结果的符号位设置,XOR不同输入取1,相同输入取0。)