FizzBuzz в сборке - ошибка сегментации

Я пытаюсь написать FizzBuzz на ассемблере и постоянно вижу ошибку сегментации. До сих пор я определил, что это не мои процедуры печати (потому что я удалил их содержимое и проблема сохраняется), и ошибка скрывается где-то в основной функции.

Я получил этот вывод, когда я запускаю программу:

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

3 ответа

Решение

Это вызывает ошибку сегментации:

...
    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 не получает обратный адрес и вернется куда угодно. Измените это на 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 reg,0 это плохо Поместите условную ветвь внизу цикла, когда это возможно.

mov rax, 1 это пустая трата байтов инструкции по сравнению с mov eax, 1 и это помечено yasm, что не оптимизирует это для вас во время сборки. (nasm сам делает.) Настройка 64-битных регистров, а затем с помощью int 0x80 32-битная совместимость ABI еще глупее.

Во-первых, глупо хранить 16-битный счетчик в памяти, но хранить его по адресу, где вы зарезервировали только один байт, вызывает проблемы.


Помимо мелочей, FizzBuzz(3,5) достаточно мал, чтобы развернуть и избежать некоторых div целиком С помощью макросов на ассемблере вы могли бы легко создать полностью развернутый цикл с выводами LCM(fizz,buzz) на цикл (т.е. 15 в данном случае); достаточно для повторения шаблона, поэтому вам не нужны никакие условия.

Вы можете избежать div без развертывания с помощью счетчиков вниз, чтобы обнаружить count%5==0 а также count%3==0, 16-битный DOS код-гольф @ anatolyg FizzBuzz делает это. Это действительно распространенная техника - делать что-то каждый раз, когда происходит что-то еще. например, события счетчика производительности работают таким образом.


Вот моя попытка создать эффективный FizzBuzz (для AMD64 Linux) без использования библиотек. только write(2) а также exit_group(2)

Там нет компилятора, поэтому, если вы хотите хороший код, вы должны написать его самостоятельно. Вы не можете просто надеяться, что компилятор сделает что-то хорошее с i%3 в цикле (чего не происходит, для большинства компиляторов).

Код развивался во время написания. Как обычно, начало реализации одного способа дает вам лучшие идеи, когда вы видите, что ваша первая идея требует большего или меньшего количества инструкций, чем вы ожидали.

Я развернул 3 (Fizz), чтобы удалить все проверки counter%3, Я занимался counter%5 чеки с обратным отсчетом от 5 вместо деления. Это по-прежнему требует немалой логики, которая могла бы исчезнуть при полном развороте до точки, где шаблон повторяется (LCM(3,5)). Код целых чисел в коде ASCII может быть в функции или встроен в развернутый цикл для очень раздутого кода.

Я держу все в регистрах (включая константы fizz\n а также buzz\n). Там нет загрузок, и хранит только в буфер. Многие из регистров устанавливаются вне цикла, а не с mov Непосредственно перед использованием. Это требует хороших комментариев, чтобы отслеживать, что вы положили куда.

Я добавляю символы в буфер, который мы write(2) после каждого fizzbuzz\n линия. Это самый длинный цикл, который происходит естественным образом в логике программы, и означает, что нам нужно только syscall код в одном месте.

В реальной программе, которая может записывать в файл или канал, было бы лучше использовать стратегию C stdio по использованию гораздо большего буфера в этом случае. (Многие ~100-байтовые записи намного хуже, чем записи 4096B.) Тем не менее, я подумал, что это был интересный выбор между традиционным printf на каждой итерации или накоплением всей строки в один большой буфер. Я использовал статический буфер вместо того, чтобы резервировать пространство стека, потому что я пишу целую программу, а не функцию, которая должна избежать потери памяти после возврата. Кроме того, он позволил мне использовать 32-битный размер операнда для приращения указателя, чтобы сохранить байты кода (префиксы REX).

Было бы довольно легко накапливать несколько блоков, пока вы не дойдете до точки, где следующая группа может пройти за конец буфера. т.е. сравнить текущую позицию с buffer_end - BUZZMOD*FIZZMOD*9, Оптимизация системных вызовов ввода / вывода - это, очевидно, широкая тема, и этой версии достаточно для демонстрации накопления строки в буфере.

;  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)                           = ?

Проверка правильности:

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

Развертывание над Buzz (5) и использование обратного счетчика для Fizz, вероятно, будет хуже. Моя версия имеет 64-битный магазин fizz\n\0\0\0 а затем ветка, чтобы решить, следует ли хранить buzz\n\0\0\0 перекрытие для производства fizzbuzz\n, Другой способ будет иметь ветку, чтобы решить, хранить ли fizz (перевод строки не требуется, так что это может быть 32-битный магазин). Тогда это будет безусловно хранить buzz\n\0\0\0, Однако, поскольку FIZZMOD меньше, чем BUZZMOD, это означает более частые сбросы обратного счетчика и больше проверок, чтобы увидеть, нужно ли нам печатать число вместо строки на этой итерации. Известно, что каждая третья строка fizz\n или же fizzbuzz\n означает, что более простой код для этого выполняется чаще.

Если пересекающиеся хранилища являются проблемой, весь этот алгоритм облажался, и это только один из многих. Кроме того, мы могли бы просто ветвиться перед сохранением fizz\n и добавление 5. Затем в fizzbuzz\n случай, мы делаем два магазина и добавляем 9. Это также отделяет dec/jcc от cmp/jcc внизу main_loop Таким образом, они могут надеяться, что оба макро-предохранителя на предварительном Haswell. IIRC, у некоторых процессоров есть предикторы ветвлений, которым действительно не нравятся множественные ветви, действительно близкие друг к другу.


Дальнейшие улучшения, оставленные в качестве упражнения для читателя:

  • В соответствии buzz_or_number и, возможно, превратить его в цикл (итерации FIZZMOD-1)

  • Кроме того, он может, вероятно, меньше переходить, и другие незначительные улучшения. Это своего рода версия 1.1: работающая, протестированная, с некоторыми комментариями и наблюдениями, добавленными во время написания этого ответа, но на самом деле не улучшающими код, в отличие от того, что я изначально решил, было достаточно хорошо, чтобы увидеть, работает ли он.

  • Сделайте его более гибким, написав цикл очистки (или макросы ассемблера) для последнего LASTCOUNT % FIZZMOD линий, вместо того, чтобы предполагать, что это 1 строка. Код очистки является обратной стороной для развертывания.

  • Я использовал div на 10, чтобы преобразовать счетчик в строку. Лучшая реализация будет использовать мультипликативный обратный, как компиляторы генерируют для небольших константных делителей (реализовано в этом случае с LEA).

    Еще одна стратегия, на которую стоит обратить внимание, - это уменьшение силы приращения последовательности цифр ASCII (хранящихся в регистре). Эту технику будет сложнее распространить на числа с большим количеством цифр. Хранение их в порядке печати (самая значимая цифра в младшем байте) делает перенос между цифрами против нас, а не против нас. (например, если бы они были в естественном порядке, вы могли бы add eax, 256-10 для исправления младшего разряда и увеличения старшего разряда с помощью переноса.) Возможно, стоит сохранить его таким образом, но BSWAP следует сохранить. Держать \n встроенный в реестр, поэтому он занимает только один магазин, может не стоить того. Обнаружение и обработка 1-значного числа, превращающегося в 2-значное число, достаточно плохо.

    В 32-битном режиме мы могли бы использовать AAA инструкция делать десятичный перенос после увеличения. Однако, несмотря на мнемонику, он работает на BCD (0-9), не ASCII ('0'-'9'), и, кажется, не облегчает распространение переноса на 3-ю цифру. Неудивительно, что AMD удалила его для AMD64. Это проверяет AF флаг для определения выполнения младших 4 битов, но это полезно только для DAA где у вас есть две цифры BCD, упакованные в один байт, и когда вы добавляете неизвестные значения, не увеличивается. В этом случае вы просто проверяете al >= 10,


Моя первая версия почти сработала в первый раз (после исправления нескольких синтаксических ошибок, чтобы он собирался, и глупого сбоя, который потребовал пару минут для отладки IIRC): fizz\nbuzz\n в fizzbuzz\n случай, и он поменял цифры. Я постоянно забываю, что сначала нужно хранить строки цифр с самой старшей цифрой, а не байтами в двоичном порядке с прямым порядком байтов.


Альтернативные способы сделать вещи

Я решил не использовать версию без разветвления кода преобразования ASCII, состоящего из 1 цифры и 2 цифр, поскольку потребовалось много инструкций. Также ветка должна очень хорошо прогнозировать.

  ;; 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 и был удален для AMD64 вместе с другими инструкциями BCD/ASCII. Это удобно для проверки делимости на 5 без привязки регистра для делителя или траты инструкции внутри цикла. Это немного быстрее, чем div r/m8 и устанавливает флаги в соответствии с остатком (в al: его результаты обращены вспять, по сравнению с div).

Гольф Анатолий 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

Используйте отладчик, чтобы пошагово выполнить свой код и посмотреть, где он идет не так.

С первого взгляда уже очевидно, что ты уничтожаешь ax (может быть, вы не знаете, что ax изготовлена ​​из ah а также al?). Также вы переходите к функциям, а не вызываете их, это, вероятно, является причиной неисправностей.

Другие вопросы по тегам