【问题标题】:Calling printf() in assembly causes a 'floating point exception'在汇编中调用 printf() 会导致“浮点异常”
【发布时间】:2020-02-17 20:43:36
【问题描述】:

我有以下代码:

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() 时,它并不总是失败。


从printax 和printbx 中删除printf() 调用给了我

$ ./1
0

更新:如果我删除 div cx 行,异常也会消失。然后我得到以下输出:

$./1
4
9
0
0
0

但即使我添加它也不会消失

mov cx, 1
mov ax, 1

在div cx 之前。

【问题讨论】:

  • 这里唯一可能导致 FPE 的行是 div bx
  • 哇。您已经像问题代码中的所有内容一样进行了更改。请创建一个minimal reproducible example。
  • 那么当您在调试器下运行它时(提示提示),哪条指令触发了异常,寄存器值是多少?这应该可以帮助您弄清楚发生了什么。
  • 一个提示:再次阅读单操作数 div 指令的作用。
  • 异常命名存在问题:在 Linux for x86 CPU 中,“浮点异常”通常确实描述了浮点问题,但它描述了整数除以零的情况。整数除法!

标签: c linux assembly printf nasm


【解决方案1】:

再看看x86-64 ABI calling conventions。具体来说,在第 15 页:

注册 %rbp、%rbx 和 %r12 到 %r15 “属于”调用函数,被调用函数是 需要保持他们的价值。换句话说,被调用的函数必须保留 这些寄存器为其调用者的值。剩余的寄存器“属于”被调用 功能。如果调用函数想要在整个 函数调用,它必须将值保存在其本地堆栈帧中。

所以当你调用printf 时,你必须假设除了rbp, rbx, r12..r15 之外的所有寄存器都被破坏了。这对您的程序有两个影响:

  • 您尝试通过将 printf 的值存储在 si 寄存器中然后将其放回来保存 ax .

  • 您的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 似乎可以正常工作,也许它可以用于调试,但您不应该计划以这种方式编写程序用于生产。

【讨论】:

  • 幸运的是,避免溢出很容易:只有在cx=0 时,DX=0 的dx:ax / cx(就像您通常想要的那样)才会溢出。一般来说,您只需要检查cx!=0 和dx < cx。同样适用于任何操作数大小,使用适当的寄存器作为高半和除数。有符号除法 (idiv) 有更多极端情况,包括 INT_MIN / -1,我想你需要一些绝对值的东西来检查 |high_dividend| < |divisor| 的一般情况,而不仅仅是符号扩展 [E/R]使用cdq 或其他任何方式将 AX 转换为 [E/R]DX。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-05-31
  • 1970-01-01
  • 2016-06-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多