Вызов printf () в сборке вызывает «исключение с плавающей точкой» - PullRequest
2 голосов
/ 21 октября 2019

У меня есть следующий код:

extern printf

section .data

A db 1
B db 2
C db 3
D db 4
E db 5

fmt  db "%d",10,0
fmt2 db "An overflow has occured",10,0

section .text

    global _start

_start:
    xor rax, rax
    xor rbx, rbx
    xor rdx, rdx

    xor ax, ax
    mov al, [B]
    mul ax
    jo over
    call printax
    mov bx, ax

    xor ax, ax
    mov al, [C]
    mul ax
    jo over
    call printax

    xor cx, cx
    mov cl, [C]
    mul cx
    jo over
    call printax

    xor cx, cx
    mov cl, [E]
    div cx
    jo over
    call printax
    xor dx, dx

    xor al, al
    mov rdi, fmt
    call printf
    jmp exit

over:
    mov rdi, fmt2
    xor al, al
    call printf

exit:
    mov rax, 60
    mov rdi, 0
    syscall

printbx:
    xor rsi, rsi
    mov si, bx
    mov cl, al
    xor al, al
    mov rdi, fmt
    call printf
    mov al, cl
    xor si, si
    ret

printax:
    xor rsi, rsi
    mov si, ax
    xor al, al
    mov rdi, fmt
    call printf
    mov ax, si
    xor si, si
    ret

Я компилирую его с помощью nasm -f elf64 1.asm и связываю его с помощью ld -dynamic-linker /lib/ld-linux-x86-64.so.2 1.o -o 1 -lc. Когда я выполняю двоичный файл, я получаю

$ ./1
4
9
0
Floating point exception (core dumped)

printf(), который не всегда терпит неудачу, когда я вызываю его в коде сборки.


Удаление printf() вызовов из printax и printbx дает мне

$ ./1
0

Обновление: исключение также исчезает, если я удаляю строку div cx. Затем я получаю следующий вывод:

$./1
4
9
0
0
0

Но он не исчезает, даже если я добавлю

mov cx, 1
mov ax, 1

до div cx.

1 Ответ

2 голосов
/ 21 октября 2019

Еще раз взглянем на x86-64 соглашения о вызовах ABI . В частности, на странице 15:

Регистры% rbp,% rbx и от% r12 до% r15 «принадлежат» к вызывающей функции, и вызываемая функция должна сохранять свои значения. Другими словами, вызываемая функция должна сохранять значения этих регистров для своего вызывающего. Остальные регистры «принадлежат» вызываемой функции. Если вызывающая функция хочет сохранить такое значение регистра при вызове функции, она должна сохранить значение в своем локальном фрейме стека.

Поэтому, когда вы вызываете printf, вы должны предполагать, что все регистрыкроме rbp, rbx, r12..r15 забиты. Это имеет два эффекта для вашей программы:

  • Вы пытаетесь сохранить ax вокруг вызова на printf, сохраняя его значение в регистре si и затем возвращая его позже, но это не помогает, потому что si может быть перекрыт.

  • Ваша инструкция div cx делит содержимое dx:ax на cx, но dx также можетбыли забиты

Последнее является специфической причиной SIGFPE (которая, несмотря на его имя, также возникает при переполнении деления integer ), по крайней мере, в моих тестовых прогонах. После возврата printf, dx содержит какое-то огромное число, такое, что dx:ax, разделенное на cx, не помещается в 16 бит, что является переполнением.

(Это также объясняет, почему произошел сбой, когда вы ответили на вызов printf - его больше не было, чтобы заглушить ваши регистры.)

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

Несколько других замечаний:

  • Перед последним вызовом printf (непосредственно перед jmp exit) вы ничего не загружаете в rsi, поэтому выдается значение мусора.

  • 16-битная арифметика обычно не рекомендуется использовать на x86-32 или x86-64, если для этого нет веских причин. Это не быстрее и раздувает ваш код с префиксами размера операнда. Лучше выполнять всю работу с 32-битной арифметикой.

  • Поскольку вы используете свою собственную точку входа, а не позволяете библиотеке C вызывать ваш main, это означает, что libc имеетНе было возможности запустить собственный код инициализации. Следовательно, необязательно безопасно вызывать любую функцию libc, в частности, stdio и функции размещения. Похоже, что printf в этом случае работает нормально, и, возможно, все в порядке для отладки, но вы не должны планировать писать свою программу таким образом для производства.

...