Segmentation fault in x86 while computing floating point sequence

时间:2016-05-27 08:09:24

标签: x86 nasm

Assembly program to compute sum of given sequence: 1+(1/1!)+(1/2!)+...+(1/10!) using floating point registers. Why do I get a segmentation fault(Core dumped) error in the following program?

global  main
main:
    mov eax,2
    mov ebx, 2
    mov edx, 2
.fact:
    fld dword [ecx]
    fmul dword [ebx]
    fstp dword [ecx]
    fld dword [ebx]
    fadd dword [one]
    fstp dword [ebx]
    fld dword [ebx]
    fld dword [eax]
    fcomi st0,st1
    ja .fact
.sum:
    fld dword [one]
    fdiv dword [ecx]
    fadd dword [edx]
    fstp dword [edx]
    fld dword [ten]    
    fld dword [eax]
    fcomi st0,st1
    ja .exit
    fadd dword [one]
    mov ebx,2
    jmp .fact
.exit:
    push edx
    push    dword msg       
    call    printf  
    add     sp, 8

1 个答案:

答案 0 :(得分:2)

您有正确的想法,但我可以看到此代码存在许多问题,导致其无法按预期运行:

您需要分配内存

正如在评论中已经提到的那样,这些浮点指令中的括号表示加载/存储到内存。例如,语句fld dword [ecx]不会加载内容ecx st0ecx。它将地址 st0处的32位浮点值加载到2。现在,在某些情况下,您尝试从地址eax进行读/写。

因此,要使所有这些加载和存储工作,您需要在堆栈上分配变量。您使用其中的四个(ecxedxebxsub esp, 0x10 lea eax, [esp] lea ecx, [esp+4] lea edx, [esp+8] lea ebx, [esp+0xC] ),每个大小为四个字节,因此您可以保持寄存器的使用方式目前,如果你开始做例如:

lea

如果您不熟悉lea,则会在未从内存中读取的情况下加载地址 mov eax, esp是唯一的 1 x86指令,其中括号不代表内存访问。例如,第二行在功能上等同于add esp, 0x10

最后,记得在离开例程(mov ebx, 2)之前将堆栈指针放回原位。

您没有正确设置浮点常数

正如上一节所述,像ebx这样的指令不能达到你想要的效果,因为我们希望mov dword [ebx], 2指向内存中我们可以存储浮点数的地址。

但即使你写2ebx存储到地址 [ebx],这仍然是错误的,因为在这个例行程序中,我们会对待{{} 1}}作为一个浮点数,而你存储到该位置的2 2的整数表示。

fild指令从内存加载一个整数,将其转换为浮点值,并将其存储在FPU堆栈的顶部。例如,以下指令将整数转换为float:

mov dword [ebx], 2  ; Now [ebx] contains 2
fild dword [ebx]    ; Now st0 contains 2.0
fstp dword [ebx]    ; Now [ebx] contains 2.0

您正在溢出FPU堆栈

每次使用fld时,都会将值推送到FPU堆栈。每个fstp都会弹出FPU堆栈的值。 (那是&{39; p'在fstp中的含义)您使用faddfmulfdiv的方式只需替换当前位于FPU堆栈顶部的值(又名st0)。这一切都很好,但是fcomi并没有从FPU堆栈中弹出任何东西。因此,每当您完成循环并执行两个fld s后跟fcomi时,您就会在堆栈中添加两个值并将其保留在那里。

FPU堆栈只能存储8个值(st0st7)。因此,如果所有八个都被填充并且您尝试将另一个值推送到FPU堆栈(例如,使用fld),那么您将获得 FPU堆栈溢出异常和负载会失败。

还有另一条比较指令fcomip,但它只会从FPU堆栈中弹出一个值。要弹出另一个,您可以使用fstp st0。该指令基本上意味着"将st0存储到st0,然后将最高值从FPU堆栈中弹出。"

printf不支持浮动

When passing a float to a variadic function, it is automatically promoted to a double.由于printf是一个可变函数,%f说明符(当然然后)也需要double。因此,在函数结束时,您希望将[edx](不是edx!)的内容打印为double。由于printf需要一个双倍(64位宽),push dword [edx]之类的东西不会削减它。

将值加载到FPU堆栈时,它们以尽可能高的精度存储。将它们存储到内存中时(通过fstp),您可以指定精度(以及格式和宽度)。当您说fstp dword [address]时,您指定dword宽度,即32位浮点数。如果您需要double,只需指定qword

我们可以将值直接存储到堆栈中。所以尝试类似的事情:

sub esp, 8       ; Make space for the 64-bit double we're about to store
fld dword [edx]  ; Load the 32-bit float we want to print
fstp qword [esp] ; Store the 64-bit double to the space we just allocated

请记住,在printf之后,您需要重新调整堆栈指针(不仅仅是8个字节!)。另请注意,任何32位模式下的堆栈指针都是 esp ,而不是 sp

您需要初始化产品

你在那里的第四条指令是fld dword [ecx]。我们尚未在此例程中初始化ecx。由于您使用[ecx]来存储针对您的阶乘的产品,因此您希望在每次开始1.0之前将其设置为.fact }循环。

你的阶乘循环上的条件是一个

您似乎正在通过使用[eax]启动主循环计数器(2)并使用2 = 1/0! + 1/1!初始化您的总和来进行优化。在您运行.fact循环之前,您将[ebx]初始化为2,可能是另一个优化,以避免乘以1.在伪代码中,您的算法如下所示:

// B is initialized to 2
.fact:
    C = C * B
    B = B + 1
    if A > B: goto .fact

基本上(如果C从1开始),C是从2A-1的所有整数的乘积,总是包括2无论如何什么。

这并不是你想要的。请记住,您希望在总和中加入1/10!,如上所述,需要使用.fact执行A=11。另外,请考虑此算法中CA的不同值的结果:

A:  2  3  4  5   6   ...
C:  2  2  6  24  120 ...

参考上面的图表,根据算法的其余部分,最终总和将是2 + 1/2 + 1/2 + 1/6 + 1/24 + 1/120 ...。这将由1/2关闭。

您可以通过更改一行来解决这两个问题。

额外的东西

  • 您需要以某种方式退出程序,否则您将继续执行超出程序范围的指令,直到您发生段错误
  • 有关于将常用值加载到FPU堆栈的特殊说明。其中两个可能对您有用:fldz加载+0.0fld1加载1.0
  • 任何时候你想要回答问题"为什么我的程序会出现段错误?" 你应该在调试器中运行程序。它将向您显示segfault发生的确切指令,并允许您检查程序的状态(寄存器,内存的内容等)。在调试器中学习一些快速命令的回报非常值得:)

1 这是您唯一需要关心的问题。要迂腐,nop指令也可以采用地址参数,但正如您所料,它实际上并不访问内存,因为它是 no-op