代码块Ver.16.01在程序运行周期中崩溃

时间:2016-12-12 20:10:06

标签: c++ x86 g++ inline-assembly

我有一个程序已被证明可以在较旧版本的代码块上运行(版本13.12),但是当我在较新版本(版本16.01)上尝试时它似乎不起作用。该程序的目的是输入两个整数,然后将添加,mult等。它使用asm代码,我是新来的。我的问题是为什么在我输入2个整数并按回车键后,Windows已停止响应?

以下是代码:

//Program 16

#include <stdio.h>
 #include <iostream>
 using namespace std;

int main() {

int arg1, arg2, add, sub, mul, quo, rem ;

cout << "Enter two integer numbers : " ;
cin >>  arg1 >> arg2 ;
cout << endl;

  asm ( "addl %%ebx, %%eax;" : "=a" (add) : "a" (arg1) , "b" (arg2) );
  asm ( "subl %%ebx, %%eax;" : "=a" (sub) : "a" (arg1) , "b" (arg2) );
 asm ( "imull %%ebx, %%eax;" : "=a" (mul) : "a" (arg1) , "b" (arg2) );

asm ( "movl $0x0, %%edx;"
"movl %2, %%eax;"
"movl %3, %%ebx;"
"idivl %%ebx;" : "=a" (quo), "=d" (rem) : "g" (arg1), "g" (arg2) );

cout<< arg1 << "+" << arg2 << " = " << add << endl;
 cout<< arg1 << "-" << arg2 << " = " << sub << endl;
cout<< arg1 << "x" << arg2 << " = " << mul << endl;
cout<< arg1 << "/" << arg2 << " = " << quo << "  ";
 cout<< "remainder " << rem << endl;

return 0;
}

2 个答案:

答案 0 :(得分:4)

正如迈克尔所说,你的问题可能来自你的第四份asm陈述写得不正确。

在编写内联asm时,首先需要了解的是寄存器是什么以及如何使用它们。寄存器是x86汇编程序编程中的一个基本概念,所以如果你不知道它们是什么,那么你就该找到一个x86汇编语言入门。

一旦你有了这个,你需要明白,当编译器运行时,在它生成的代码中使用那些寄存器。例如,如果你for (int x=0; x<10; x++),x(可能)最终会进入寄存器。那么如果gcc决定使用ebx来保存'x'的值,那么会发生什么呢?然后你的asm语句会在ebx上踩踏,在其中添加一些其他值? gcc没有'解析'你的asm来弄清楚你在做什么。关于你的asm做什么的唯一线索是在asm指令之后列出的那些约束。

迈克尔的意思是当他说“第4个ASM区块没有在clobber列表中列出”EBX“时(但其内容被破坏)”。如果我们看看你的asm:

asm ("movl $0x0, %%edx;"
     "movl %2, %%eax;"
     "movl %3, %%ebx;"
     "idivl %%ebx;" 
  : "=a" (quo), "=d" (rem) 
  : "g" (arg1), "g" (arg2));

你看到第3行正在将一个值移动到ebx中,但是后面的约束中没有任何内容表明它会被更改。您的程序崩溃的事实可能是由于gcc使用该寄存器进行其他操作。最简单的修复可能是“在clobber列表中列出EBX”:

asm ("movl $0x0, %%edx;"
     "movl %2, %%eax;"
     "movl %3, %%ebx;"
     "idivl %%ebx;" 
  : "=a" (quo), "=d" (rem) 
  : "g" (arg1), "g" (arg2)
  : "ebx");

这告诉gcc ebx可能被asm更改(也称为'clobbers'),并且当asm语句开始时它不需要具有任何特定值,并且不具有任何特定值当asm退出时。

然而,尽管这可能是“最简单的”,但它并不一定是最好的。例如,我们可以使用"g"约束来代替使用"b"约束arg2:

asm ("movl $0x0, %%edx;"
     "movl %2, %%eax;"
     "idivl %%ebx;" 
  : "=a" (quo), "=d" (rem) 
  : "g" (arg1), "b" (arg2));

这让我们摆脱了movl %3, %%ebx语句,因为gcc在调用asm之前会确保值在ebx中,我们不再需要它了。

但为什么要使用ebx? idiv在那里不需要任何特定的注册,也许gcc已经在使用ebx了。让gcc选择一些不使用的注册表怎么样?我们使用"r"约束来执行此操作:

asm ("movl $0x0, %%edx;"
     "movl %2, %%eax;"
     "idivl %3;" 
  : "=a" (quo), "=d" (rem) 
  : "g" (arg1), "r" (arg2));

请注意,idiv现在使用%3,这意味着“使用(从零开始)参数#3中的东西。”在这种情况下,这是包含arg2的寄存器。

然而,我们仍然可以做得更好。正如您在之前的asm语句中已经看到的那样,您可以使用"a"约束来告诉gcc将特定变量放入eax寄存器中。这意味着我们可以做到这一点:

asm ("movl $0x0, %%edx;"
     "idivl %3;" 
  : "=a" (quo), "=d" (rem) 
  : "a" (arg1), "r" (arg2));

再次减少1条指令,因为我们不再需要将值移动到eax中。那么movl $0x0, %%edx那件事怎么样?好吧,我们也可以摆脱它:

asm ("idivl %3"
  : "=a" (quo), "=d" (rem) 
  : "a" (arg1), "r" (arg2), "d" (0));

这使用"d"约束在执行asm之前将0放入edx。这将我们带到了我的最终版本:

asm ("idivl %3"
  : "=a" (quo), "=d" (rem) 
  : "a" (arg1), "r" (arg2), "d" (0)
  : "cc");

这说:

  • 在输入时,将arg1放入eax,将arg2放入某个寄存器(我们将使用%3引用),将0放入edx。
  • 在输出时,eax将包含商,edx将包含余数。这就是idiv指令的工作原理。
  • “cc”clobber告诉gcc你的asm修改了标志寄存器(eflags),idiv作为副作用。

现在,尽管已经描述了这一切,但我通常认为使用内联asm是一个坏主意。它很酷,功能强大,它可以让您深入了解gcc编译器的工作原理。但是看看你“必须知道”所有奇怪的事情才能使用它。正如你已经注意到的那样,如果你发现任何错误,可能会发生奇怪的事情。

所有这些事情都记录在gcc的文档中。简单约束(如"r""g")是doc'ed here。 x86的特定寄存器约束位于“x86系列”here中。所有asm功能的详细说明是here。因此,如果你必须使用这些东西(例如,如果你支持一些使用它的现有代码),那么信息就在那里。

但是有一个更短的阅读here,它为您提供了使用内联asm的完整原因列表。那是我推荐的读物。坚持使用C,让编译器为你处理所有注册垃圾。

PS我在这时:

asm ( "addl %2, %0" : "=r" (add) : "0" (arg1) , "r" (arg2) : "cc");
asm ( "subl %2, %0" : "=r" (sub) : "0" (arg1) , "r" (arg2) : "cc");
asm ( "imull %2, %0" : "=r" (mul) : "0" (arg1) , "r" (arg2) : "cc");

查看gcc docs,了解在输入操作数中使用数字意味着什么。

答案 1 :(得分:4)

David Wohlferd就如何更好地使用 GCC 扩展程序集模板来完成现有代码的工作提供了一个非常好的答案。

可能会出现一个问题,为什么所提供的代码无法使用Codeblocks 16.01 w / GCC ,因为它可能以前有效。现在代码看起来很简单,那么可能出现什么问题呢?

我建议最好的方法是学习使用调试器并在Codeblocks中设置断点。这很简单(但超出了这个答案的范围)。您可以在Codeblocks documentation中了解有关调试的更多信息。

如果您使用带有Codeblocks 16.01的调试器和库存C ++控制台项目,您可能已经发现该程序在 IDIV 指令中为您提供了算术异常装配模板。这是我的控制台输出中出现的内容:

  

编程接收信号SIGFPE,算术异常。

这些代码行正如您所期望的那样:

asm ( "addl %%ebx, %%eax;" : "=a" (add) : "a" (arg1) , "b" (arg2) );
asm ( "subl %%ebx, %%eax;" : "=a" (sub) : "a" (arg1) , "b" (arg2) );
asm ( "imull %%ebx, %%eax;" : "=a" (mul) : "a" (arg1) , "b" (arg2) );

这就是问题所在:

asm ( "movl $0x0, %%edx;"
      "movl %2, %%eax;"
      "movl %3, %%ebx;"
      "idivl %%ebx;" : "=a" (quo), "=d" (rem) : "g" (arg1), "g" (arg2) );

Codeblocks可以为您做的一件事就是向您展示它生成的汇编代码。下拉Debug菜单,选择Debugging Windows >Disassembly。我强烈推荐WatchesCPU Registers窗口。

如果您使用CodeBlocks 16.01 w / GCC查看生成的代码,您可能会发现它产生了这个:

/* Automatically produced by the assembly template for input constraints */
mov    -0x20(%ebp),%eax      /* EAX = value of arg1 */
mov    -0x24(%ebp),%edx      /* EDX = value of arg2 */

/* Our assembly template instructions */
mov    $0x0,%edx             /* EDX = 0 - we just clobbered the previous EDX! */
mov    %eax,%eax             /* EAX remains the same */
mov    %edx,%ebx             /* EBX = EDX = 0. */
idiv   %ebx                  /* EBX is 0 so this is division by zero!! *

/* Automatically produced by the assembly template for output constraints */
mov    %eax,-0x18(%ebp)      /* Value at quo = EAX */
mov    %edx,-0x1c(%ebp)      /* Value at rem = EDX */

我对代码进行了评论,显然为什么这段代码不起作用。我们实际上最终在 EBX 中放置零,然后尝试将其用作 IDIV 的除数,并产生算术异常(在这种情况下除以零)。

这是因为 GCC 将(默认情况下)假设在写入输出操作数之前使用(消耗)所有输入操作数。我们从未告诉 GCC 它不可能使用与输出操作数相同的输入操作数。 GCC 认为这种情况为Early Clobber。它提供了一种机制,使用&(&符号)修饰符将输出约束标记为早期删除:

  

`&安培;&#39;

     

意味着(在一个特定的替代方案中)该操作数是一个earlyclobber操作数,它在使用输入操作数完成指令之前被修改。因此,该操作数可能不在于用作输入操作数的寄存器或任何存储器地址的一部分。

通过更改操作数以便处理早期的clobbers,我们可以将&放在输出约束上,如下所示:

"idivl %%ebx;" : "=&a" (quo), "=&d" (rem) : "g" (arg1), "g" (arg2) );

在这种情况下,arg1arg2不会通过标有&的任何操作数传入。这意味着此代码将避免对输入操作数arg1arg2使用 EAX EDX

另一个问题是 EBX 会被您的代码修改,但您不会告诉 GCC 。您只需将 EBX 添加到程序集模板中的clobber列表中,如下所示:

"idivl %%ebx;" : "=&a" (quo), "=&d" (rem) : "g" (arg1), "g" (arg2) : "ebx");

所以这段代码应该有效,但效率不高:

asm ( "movl $0x0, %%edx;"
      "movl %2, %%eax;"
      "movl %3, %%ebx;"
      "idivl %%ebx;" : "=&a" (quo), "=&d" (rem) : "g" (arg1), "g" (arg2) : "ebx");

生成的代码现在看起来像:

/* Automatically produced by the assembler template for input constraints */
mov    -0x30(%ebp),%ecx      /* ECX = value of arg1 */
mov    -0x34(%ebp),%esi      /* ESI = value of arg2 */

/* Our assembly template instructions */
mov    $0x0,%edx             /* EDX = 0 */
mov    %ecx,%eax             /* EAX = ECX = arg1 */
mov    %esi,%ebx             /* EBX = ESI = arg2 */
idiv   %ebx

/* Automatically produced by the assembler template for output constraints */
mov    %eax,-0x28(%ebp)      /* Value at quo = EAX */
mov    %edx,-0x2c(%ebp)      /* Value at rem = EDX */

这次arg1arg2的输入操作数不共享与内联汇编模板中的MOV指令冲突的相同寄存器。

为什么其他(包括较旧的)GCC版本有效?

如果 GCC 使用 EAX EDX 以及 EBX 以外的寄存器为{{1然后它会起作用。}和arg1操作数。但它可能有效的事实只是运气。要查看旧版代码块及其附带的 GCC 所发生的情况,我建议使用与上述相同的方式查看在该环境中生成的代码。

早期的破坏和注册破坏通常是扩展汇编程序模板可能很棘手的原因,并且应该谨慎使用扩展汇编程序模板的原因,特别是如果您不具备扎实的理解。

您可以创建看似有效的代码,但编码不正确。不同版本的 GCC 或甚至不同的优化级别可能会改变代码的行为。有时这些错误可能是如此微妙,以至于随着程序的增长,错误会以其他可能难以追踪的方式表现出来。

另一个经验法则是,并非您在互联网上找到的所有代码都没有错误,并且教程中经常忽略扩展内联汇编的微妙复杂性。我发现您使用的代码似乎基于此Code Project。遗憾的是,作者并未对所涉及的内部情况有透彻的了解。代码可能当时有效,但现在不一定。