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

person kacpr    schedule 05.02.2015    source источник


Ответы (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
...
person rkhb    schedule 05.02.2015

Этот ответ оказался намного длиннее, чем я планировал, что-то вроде учебника по написанию эффективного ассемблера. то есть как сделать простую задачу сложной.


На случай, если кого-то заинтересует обзор кода попытки реализации и версия с множеством трюков на ассемблере:

Есть так много маленьких способов, которыми это могло бы быть лучше, например. оставить 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 (шипение, жужжание) на цикл (т.е. 15 в данном случае); достаточно для повторения шаблона, поэтому вам не нужны никакие условия.

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


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

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

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

Я развернул 3 (Физз), чтобы убрать все проверки 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, встроенного в реестр, чтобы он занимал только одно хранилище, может быть нецелесообразным. Обнаружение и обработка однозначного числа, превращающегося в двузначное число, уже достаточно плохо.

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


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


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

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

FizzBuzz от Anatolyg использует 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
person Peter Cordes    schedule 28.05.2016
comment
Глядя на это снова позже, я действительно должен был оптимизировать call/ret во внутреннем цикле, продублировав код, вместо того, чтобы сохранять небольшой размер кода, делая его отдельной функцией. Развертывание и обратный счетчик — правильная идея, но мне, вероятно, следовало бы также избегать div для деления на 10. Либо использовал мультипликативную обратную, либо увеличивал цифры ASCII и обрабатывал перенос в 10 вместо 0xFF с помощью сравнения / cmov / adc. (Как в цикле adc повышенной точности в codegolf.stackexchange.com/a/135618/30206 Extreme Fibonacci) . - person Peter Cordes; 22.08.2017
comment
Связано: Высокопроизводительный Fizz Buzz на codegolf: в основном трюки, позволяющие избежать повторного преобразования целочисленной строки в строку путем развертывания шаблона и последующего добавления ведущие цифры. Но, как и в этой версии, сохранение в буфер. - person Peter Cordes; 20.12.2020

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

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

person Jester    schedule 05.02.2015
comment
Можете ли вы порекомендовать отладчик сборки с пошаговым выполнением? Я пишу это, используя только блокнот ++ и все, что выбрасывает компилятор (чего в случае segfault нет). спасибо за обе подсказки. можете объяснить, почему переходить к функциям (ну, меткам) неправильно? - person kacpr; 05.02.2015
comment
Переход к меткам — это нормально, но переход к чему-то, оканчивающемуся на ret, — нет — ret ожидает адрес возврата в стеке, которого нет, если вы используете переходы (условные или иные). Вы можете либо заменить ret прыжком назад, либо использовать call. - person Jester; 05.02.2015