装配中的 FizzBu​​zz - 分段错误

FizzBuzz in assembly - segmentation fault

我正在尝试在 Assembly 中编写 FizzBu​​zz,但我一直看到分段错误。到目前为止我已经确定这不是我的打印例程(因为我已经删除了它们的内容并且问题仍然存在)并且错误隐藏在主函数的某个地方。

我在 运行 程序时得到了这个输出:

fizzSegmentation fault

让我相信使用除法和查找余数不是问题。但我可能是错的,我已经两年没做过组装了...

SECTION .data
global _start
    fizz: db "fizz", 4
    buzz: db "buzz", 4

SECTION .bss
    counter: resb    1

SECTION .text
_start:

    mov ax,0
    mov [counter],ax

main_loop:

    cmp ax,100          ;from 0 to 100
    je  exit            ;
    mov bl,3            ;divisor
    mov ah,0            ;here will be a remainder
    div bl              ;divide
    cmp ah,0            ;compare the remainder with 0
    je  print_fizz      ;print fizz if they equal
    mov bl,5            ;new divisor
    mov ah,0            ;do I have to do it every time?
    div bl              ;divide
    cmp ah,0            ;compare the remainder with 0
    je  print_buzz      ;print buzz if they equal
    jmp print_ax        ;print contents of ax if not
    inc ax              ;increment ax
    jmp main_loop       ;jump to label

print_ax:
    ret

print_fizz:
    ret

print_buzz:
    ret

exit:
    mov rax,1
    mov rbx,0
    int 80h
    ret

我正在编译使用:

yasm -f elf64 -o fizzbuzz.o fizzbuzz.asm
ld -d -o fizzbuzz fizzbuzz.o

使用调试器单步执行您的代码,看看哪里出错了。

一眼就看出你在破坏ax(也许你不知道ax是由ahal组成的?) .你也跳转到函数而不是调用它们,这可能是错误的原因。

这是导致分段错误的原因:

...
    je  print_fizz      ;print fizz if they equal
...
    je  print_buzz      ;print buzz if they equal
    jmp print_ax        ;print contents of ax if not
...

print_ax:
    ret

print_fizz:
    ret

print_buzz:
    ret
...

由于您跳转到函数,ret 没有 return 地址并且会 return 任何地方。将其更改为 call/ret 对:

...
;   je  print_fizz      ;print fizz if they equal
    jne .1              ;skip if not equal
    call print_fizz
    .1:
...

;   je  print_buzz      ;print buzz if they equal
    jne .2              ;skip if not equal
    call print_buzz
    .2:

;   jmp print_ax        ;print contents of ax if not
    call print_ax
...

这将导致无限循环:

mov ax,0
mov [counter],ax

main_loop:

    cmp ax,100          ;from 0 to 100
    je  exit
    ...
    mov ah,0            ;here will be a remainder
    div bl              ;divide
    ...
    mov ah,0            ;do I have to do it every time?
    div bl              ;divide
    ...
    inc ax              ;increment ax
    jmp main_loop       ;jump to label

AX 改变了它的值,不适合保存循环计数器。我建议:

...
main_loop:

;   cmp ax,100          ;from 0 to 100
    cmp byte [counter], 100
...
;   inc ax              ;increment ax
    inc byte [counter]
    jmp main_loop       ;jump to label
...

这个答案比我计划的要长得多,有点像关于编写高效 asm 的教程。即如何将简单的问题复杂化。


如果有人对尝试实施的代码审查感兴趣,并且有很多 asm 技巧的版本:

有很多小方法可以做得更好,例如将 5 保留在 bh 中,将 3 保留在 bl 中。您不必总是使用 div bl。 AMD64 有 20 个单字节寄存器。 (al/ah、bl/bh、cl/ch、dl/dh(无 REX)和 sil、dil、... r15b(需要 REX))。

使用 16 位计数器至少会浪费字节(操作数大小前缀),并且会导致速度下降。使用 。尽可能将条件分支放在循环的底部。

mov eax, 1 相比,

mov rax, 1 是一种指令字节的浪费,并且它被标记为 ,它不会在 assemble 时为您优化它。 (nasm 本身就是这样。)设置 64 位寄存器然后使用 int 0x80 32 位兼容性 ABI 更加愚蠢。

首先将 16 位计数器存储到内存中是愚蠢的,但是将它存储到一个只保留一个字节的地址是自找麻烦。


除了小东西,FizzBuzz(3,5) 足够小,可以展开并完全避免一些 div。使用 assembler 宏,您可以轻松地生成一个完全展开的循环,每个循环都有 LCM(fizz,buzz) 输出(即在本例中为 15);足以使模式重复,因此您不需要任何条件。

您可以通过使用向下计数器检测count%5==0count%3==0来避免div而不展开。 @anatolyg's 16bit DOS code-golf FizzBuzz does that。这是一种非常常见的技术,每发生 N 次其他事情就做某事。例如性能计数器事件以这种方式工作。


这是我尝试的高效 FizzBu​​zz(适用于 AMD64 Linux),不使用任何库。只有 write(2)exit_group(2)

没有编译器,所以如果你想要好的代码,你必须自己写。你不能只希望编译器在循环中使用 i%3 做一些好的事情(无论如何它不会,)。

代码在我编写的过程中发生了很大变化。与往常一样,当您发现您的第一个想法需要比您希望的更多或更慢的指令时,开始实施一种方法会给您更好的想法。

我展开了 3 (Fizz) 以移除 counter%3 的所有检查。我用从 5 开始的倒计时而不是除法来处理 counter%5 检查。这仍然需要相当多的逻辑,完全展开到模式重复的点 (LCM(3,5))。整数到 ASCII 数字代码可以在一个函数中,或者内联到展开的循环中以获得非常臃肿的代码。

我将所有内容都保存在寄存器中(包括常量 fizz\nbuzz\n)。没有加载,仅存储到缓冲区中。许多寄存器在循环外设置一次,而不是在使用前立即设置 mov。这需要很好的评论来跟踪你放在哪里。

我将字符附加到我们在每个 fizzbuzz\n 行之后 write(2) 的缓冲区。这是程序逻辑中自然出现的最长循环,意味着我们只需要在一个地方使用 syscall 代码。

在可能写入文件或管道的真实程序中,在这种情况下最好使用 C stdio 的策略来使用更大的缓冲区。 (许多 ~100 字节的写入比较少的 4096B 写入要糟糕得多。)不过,我认为这是在每次迭代的传统 printf 或将整个字符串累积到一个大缓冲区之间的有趣选择。我使用静态缓冲区而不是保留堆栈 space 因为我正在编写一个完整的程序,而不是一个应该避免在返回后浪费内存的函数。此外,它还允许我使用 32 位操作数大小来增加指针,以节省代码字节(REX 前缀)。

累积多个块非常容易,直到您到达下一组可能超过缓冲区末尾的点。即,将当前位置与 buffer_end - BUZZMOD*FIZZMOD*9 进行比较。优化I/O系统调用显然是一个广泛的话题,这个版本足以演示在缓冲区中累积一个字符串。

;  for (count=1..100):
;  if(count%3 == 0) { print_fizz(); }
;  if(count%5 == 0) { print_buzz(); } else {
;       if(count%3 && count%5) print(count);
;; }
;  print(newline)

; We don't need pointers to these strings at all;  The strings are immediate data for a couple mov instructions
;SECTION .rodata        ; put constants in .rodata.
;    fizz: db "fizz"    ; No idea what the trailing  4  was for
;    buzz: db "buzz"

FIZZMOD equ 3                   ; only 3 works, but it would be easy to use a loop
BUZZMOD equ 5                   ; any value works
LASTCOUNT equ 100    ; max 100: we only handle two decimal digits.
; TODO: cleanup that can handle LASTCOUNT%FIZZMOD != 1 and LASTCOUNT%BUZZMOD != 0


SECTION .bss
;;; generate a string in this buffer.  (flush it with write(2) on "fizzbuzz" lines)
;    buf: resb    4096
buf: resb    FIZZMOD * BUZZMOD * 9     ; (worst case: every line is "fizzbuzz\n")

SECTION .text
global _start
_start:

    ; args for write(2).  (syscall clobbers rcx/r11,  and rax with the return value)
    mov   edi, 1                ; STDOUT_FILENO.  also happens to be __NR_write in the AMD64 Linux ABI
    mov   esi, buf              ; static data lives in the low 2G of address space, so we don't need a 64bit mov
    ;; edx = count.             ; calculated each iteration
    ;; mov eax, edi             ; also needed every time.   saves 3B vs  mov eax, imm32

    ; 'fizz' is only used once, so we could just store with an immediate there.  That wouldn't micro-fuse, and we'd have to do the newline separately
    mov   r10b, 10      ; base 10
    ;;mov   r14d, BUZZMOD  ; not needed, we don't div for this
    mov   r12, 'fizz' | 10<<32      ; `fizz\n`, but YASM doesn't support NASM's backquotes for \-escapes
    mov   r13, 'buzz' | 10<<32      ; `buzz\n`.  When buzz appears, it's always the end of a line


;;;;;;;; Set up for first iteration
    mov   ebp, BUZZMOD          ; detect count%BUZZMOD == 0 with a down-counter instead of dividing
    mov   ebx, 1                ; counter starts at 1
    mov   edx, esi              ; current output position = front of buf
ALIGN 16
main_loop:

    ;; TODO: loop FIZZMOD-1 times inside buzz_or_number, or here
    ;; It doesn't make much sense to unroll this loop but not inline buzz_or_number :/
    call  buzz_or_number
    inc   ebx

    call  buzz_or_number
    add   ebx, 2                ; counter is never printed on Fizz iterations, so just set up for next main_loop

    ;; Fizz, and maybe also Buzz
    mov   qword [rdx], r12      ; Fizz with a newline
    add   edx, 5                ; TODO: move this after the branch; adjust the offsets in .fizzbuzz

    dec   ebp
    jz   .fizzbuzz

;;.done_buzz:   ; .fizzbuzz duplicates the main_loop branch instead of jumping back here
    cmp   ebx, LASTCOUNT-FIZZMOD
    jbe   main_loop
;;;;;;;;;; END OF main_loop


.cleanup:
;;;;;;;;;;;;;;;;;;;;;  Cleanup after the loop
    ; hard-code the fact that 100 % FIZZMOD = 1 more line to print,
    ; and that 100 % BUZZMOD = 0, so the line is "buzz\n"

    mov   eax, edi              ; __NR_write
    mov   [rdx], r13            ; the final "buzz\n".
    sub   edx, buf - 5          ; write_count = current_pos+5 - buf.
    syscall                     ; write(1, buf, p - buf).
    ;; if buf isn't static, then use  add   edx, 5 / sub   edx, esi

    xor edi, edi
    mov eax, 231    ;  exit_group(0).  same as eax=60: exit() for a single-threaded program
    syscall


;;;;; The fizzbuzz case from the loop
.fizzbuzz:
;; count%BUZZMOD == 0:   rdx points after the \n at the end of fizz\n, which we need to overwrite

;; this is a macro so we can use it in buzz_or_number, too, where we don't need to back up and overwrite a \n
%macro  BUZZ_HIT 1
    mov   [rdx - %1], r13       ; buzz\n.  Next line will overwrite the last 3 bytes of the 64b store.
    add   edx, 5 - %1
    mov   ebp, BUZZMOD          ; reset the count%BUZZMOD down-counter
%endmacro

    BUZZ_HIT 1                  ; arg=1 to back up and overwrite the \n from "fizz\n"

    sub   edx, esi              ; write_count = current_pos - buf
    mov   eax, edi              ; __NR_write
    syscall                     ; write(1, buf, p - buf).  clobbers only rax (return value), and rcx,r11
    mov   edx, esi              ; restart at the front of the buffer

;;; tail-duplication of the main loop, instead of jmp back to the cmp/jbe
;;; could just be a jmp main_loop, if we check at assemble time that  LASTCOUNT % FIZZMOD != 0 || LASTCOUNT % BUZZMOD != 0
    cmp   ebx, LASTCOUNT-FIZZMOD
    jbe   main_loop
    jmp   .cleanup

;;;;;;;;;;;;;;;;;;;;;;; buzz_or_number: called for non-fizz cases
; special calling convention: uses (without clobbering) the same regs as the loop
;; modifies: BUZZMOD down-counter, output position pointer
;; clobbers: rax, rcx
ALIGN 32
buzz_or_number:
    dec   ebp
    jnz  .no_buzz              ; could make this part of the macro, but flow-control inside macros is probably worse than duplication

;; count%BUZZMOD == 0:  append "buzz\n" to the buffer and reset the down-counter
    BUZZ_HIT  0                 ; back up 0 bytes before appending
    ret

.no_buzz:             ;; get count as a 1 or 2-digit ASCII number
    ;; assert(ebx < 10);   We don't handle 3-digit numbers

    mov   eax, ebx
    div   r10b                  ; al = count/10 (first (high) decimal digit), ah = count%10 (second (low) decimal digit).
    ;; x86 is little-endian, so this is in printing-order already for storing eax

    ;movzx eax, ax            ; avoid partial-reg stalls on pre-Haswell
    ;; convert integer digits to ASCII by adding '0' to al and ah at the same time, and set the 3rd byte to `\n`.
    cmp   ebx, 9                ; compare against the original counter instead of the div result, for more ILP and earlier detection of branch misprediction
    jbe   .1digit               ; most numbers from 1..100 are 2-digit, so make this the not-taken case
    add   eax, 0x0a3030   ;;  `00\n`: converts 2 integer digits -> ASCII
    ;; eax now holds the number + newline as a 3-byte ASCII string
    mov   [rdx], eax
    add   edx, 3
    ret

.1digit:
;; Could use a 16bit operand-size here to avoid partial-reg stalls, but an imm16 would LCP-stall on Intel.
    shr   eax, 8                ; Shift out the leading 0
    add   eax, 0x000a30   ;; 1-digit numbers
    ;; eax now holds the number + newline as a 2-byte ASCII string
    mov   [rdx], ax
    add   edx, 2
    ret

这是它的运行方式:

$ strace ./fizzbuzz > /dev/null
execve("./fizzbuzz", ["./fizzbuzz"], [/* 69 vars */]) = 0
write(1, "1\n2\nfizz\n4\nbuzz\nfizz\n7\n8\nfizz\nbu"..., 58) = 58
write(1, "16\n17\nfizz\n19\nbuzz\nfizz\n22\n23\nfi"..., 63) = 63
write(1, "31\n32\nfizz\n34\nbuzz\nfizz\n37\n38\nfi"..., 63) = 63
write(1, "46\n47\nfizz\n49\nbuzz\nfizz\n52\n53\nfi"..., 63) = 63
write(1, "61\n62\nfizz\n64\nbuzz\nfizz\n67\n68\nfi"..., 63) = 63
write(1, "76\n77\nfizz\n79\nbuzz\nfizz\n82\n83\nfi"..., 63) = 63
write(1, "91\n92\nfizz\n94\nbuzz\nfizz\n97\n98\nfi"..., 40) = 40
exit_group(0)                           = ?

Correctness check:

./fizzbuzz | diff - <(perl -E'say((fizz)[$_%3].(buzz)[$_%5]or$_)for+1..100')
# no output = no difference

展开 Buzz (5) 并对 Fizz 使用递减计数器可能会更糟。我的版本有一个 64 位存储 fizz\n[=42=][=42=][=42=],然后有一个分支来决定是否存储 buzz\n[=43=][=43=][=43=] 重叠以产生 fizzbuzz\n。另一种方式是有一个分支来决定是否存储 fizz (不需要换行,所以它可以是一个 32 位存储)。然后它会无条件地存储buzz\n[=43=][=43=][=43=]。然而,由于 FIZZMOD 比 BUZZMOD 小,这意味着更频繁地重置递减计数器,并更多地检查以查看我们是否需要在本次迭代中打印数字而不是字符串。每三行已知为 fizz\nfizzbuzz\n 意味着更简单的代码运行更频繁。

如果商店重叠是个问题,那么整个算法都搞砸了,这只是众多算法中的一个。此外,我们可以在存储 fizz\n 并添加 5 之前进行分支。然后在 fizzbuzz\n 的情况下,我们进行两次存储并添加 9。这也将 dec/jcc 与 [=184= 分开] 在 main_loop 的底部,因此他们有望在 pre-Haswell 上进行宏融合。 IIRC,一些 CPU 有分支预测器,它们真的不喜欢多个分支非常靠近彼此。


进一步改进,留作 reader:

的练习
  • 内联buzz_or_number,并可能将其变成一个循环(FIZZMOD-1 迭代)

  • 除此之外,它可能会减少分支,以及其他小的改进。这有点像 1.1 版:工作、测试,在写这个答案时添加了一些评论和观察,但实际上并没有比我最初认为足够好的代码改进太多,看看它是否有效。

  • 通过为最后 LASTCOUNT % FIZZMOD 行编写清理循环(或 assembler 宏)使其更加灵活,而不是假设它是 1 行。清理代码是展开的缺点。

  • to convert the counter to a string. A better implementation would use a multiplicative inverse, like compilers generate for small constant divisors (implemented in this case with LEA).

    另一种值得考虑的策略是强度降低以递增 ASCII 数字序列(存储在寄存器中)。这种技术将更难扩展到具有更多位数的数字。按打印顺序存储它们(低字节中最重要的数字)会使数字之间的进位对我们不利而不是对我们有利。 (例如,如果它们按自然顺序排列,您可以 add eax, 256-10 更正低位并通过进位递增高位。)保持这种方式可能是值得的,但要存储 BSWAP。将 \n 嵌入寄存器中以便只占用一个存储可能不值得。检测和处理一个 1 位数字变成 2 位数字已经够糟糕了。

    在 32 位模式下,我们可以使用 AAA instruction 在递增后进行十进制进位。然而,尽管有助记符,但它适用于 BCD (0-9),而不适用于 ASCII ('0'-'9'),并且似乎不容易将进位传播到第 3 位。难怪 AMD 为 AMD64 删除了它。它检查 AF 标志以检测低 4 位的进位,但这仅对 DAA 有用,其中您将两个 BCD 数字打包到一个字节中,并且当您添加未知值时, 不递增。在这种情况下,您只需检查 al >= 10.


我的第一个版本几乎是第一次运行(在修复了一些语法错误之后 assemble,以及一个愚蠢的崩溃,花了几分钟来调试 IIRC):它打印了 fizz\nbuzz\nfizzbuzz\n 的情况下,它反转了数字。 数字字符串需要先存储最高有效位,而不像小端二进制整数中的字节。


做事的替代方法

我决定不使用 1 位与 2 位 ASCII 转换码的无分支版本,因为它需要很多指令。另外,分支应该预测得很好。

  ;; Untested
  buzz_or_number:
  ...
  .no_buzz:
    ... 
    div   r10b
 DECIMAL_TO_ASCII_NEWLINE_2DIGIT equ 0x0a3030   ; add '0' to two unpacked decimal digits, and a newline
 DECIMAL_TO_ASCII_NEWLINE_1DIGIT equ 0x000a30

    ;; hoist this out of the loop: mov   r15d, DECIMAL_TO_ASCII_NEWLINE_2DIGIT - DECIMAL_TO_ASCII_NEWLINE_1DIGIT
    xor   ecx,ecx
    cmp   ah, 1            ; set CF if ah=0 (1 digit number), otherwise clear it.  This allows sbb for a conditional add, instead of setcc
    cmovae ecx, r15d       ; 0 or the difference from 1digit to 2digit

    lea   eax, [rax+rcx + DECIMAL_TO_ASCII_NEWLINE_1DIGIT]  ; rax+=0x0a3030 or 0x000a30, without clobbering flags
    mov   [rdx], eax
    sbb   edx, -3          ; add 2 (-(-3) - 1) or 3.
    ret

在 32 位(和 16 位)模式下,有一个 一个 div 指令接受一个立即操作数 ,并使用 AL 作为被除数,而不是 AX。它 称为 AAM,并且与其他 BCD/ASCII 指令一起针对 AMD64 被删除。测试被 5 整除很方便,而无需占用除数寄存器或在循环内浪费指令。它比 div r/m8 稍快,并根据余数设置标志(在 al 中:与 div 相比,它的输出相反)。

Anatolyg's golfed FizzBuzz 在循环中使用 AAM 与 shr ax, 8 以相反的顺序一次生成一个数字,存储和递减指针。

这个版本要复杂得多,因为它使用 AAM 检查计数 %5,然后将其处理为计数 %10,而不是进行单独的除法获取 ASCII 数字。

 ;; Untested
buzz_or_number_div:
    mov   eax, ebx

    aam   5               ; al = al%5  ah = al/5.  (opposite locations from div), and sets flags according to the remainder.
    jz    print_buzz      ; tailcall


    ; fall through into print_counter
;print_counter:
    ; maybe use the result of div by 5 to get division by 10?  
    ; shifting the low bit of the quotient into bit 4 of the remainder should be faster than dividing again.
    ;; after AAM: ah = 5bit quotient (qqqqQ), al = 3bit remainder(RRR)
    ;; starting point:     ; AX = [ 000qqqqQ 00000RRR ]
    ;; desired = byte swapped as well: [ 0000QRRR 0000qqqq ]
    shl   al, 5            ; AX = [ 000qqqqQ RRR00000 ]
    shr   ax, 1            ; AX = [ 0000qqqq QRRR0000 ]
    ror   ax, 8            ; AX = [ QRRR0000 0000qqqq ]  ; simple byte-swap
    shr   ah, 4            ; AX = [ 0000QRRR 0000qqqq ]

    add   eax, ...;  convert to ascii
    ...
    ret

    ; those instructions are all single-uop 1c latency on SnB-family, but pre-Haswell will insert extra merging uops.  (And stall while doing so, on pre-SnB).
    ; and there's another partial-reg stall when we read eax

    ; It might be possible to do this bit manipulation with fewer operations, or maybe different ones.  (maybe copy ax to cx, so we can move from cl or ch to al or ah?)

;    shr   ah, 1           ; AX = [ 0000qqqq 00000RRR ]  CF=Q   ; then what? setc/shift/or?  rcl is slow, too.
;    rorx  eax, eax, 32-4  ; AX = [ qqqq0000 0RRR0000 ]  CF=Q
;  nope, seems a dead end

;    shl   ah, 3           ; AX = [ qqqqQ000 00000RRR ]
;    ror   ax, 7           ; AX = [ 0000RRRq qqqQ0000 ]
;    shr   al, 4           ; AX = [ 0000RRRq 0000qqqQ ]
;  oops, no, shifts the wrong way.

;    shl   ah, 3           ; AX = [ qqqqQ000 00000RRR ]
;    or    ah, al          ; AX = [ qqqqQRRR 00000RRR ]
;    xor   al,al           ; AX = [ qqqqQRRR 00000000 ]
;    rol   ax, 4           ; AX = [ QRRR0000 0000qqqq ]
;    shr   ah, 4           ; AX = [ QRRR0000 qqqq0000 ]
; only 3 shifts, but still partial-reg heavy.  Interesting on Haswell

;    ror   ax, 9           ; AX = [ Q00000RR R000qqqq ]  CF=Q