当您在 C++ 中使用 asm 代码操作寄存器时会发生什么情况?
What happens to registers when you manipulate them using asm code in C++?
一些代码:
int x = 1;
for(int i = 1; i < 10; i++)
{
x *= i;
__asm {
mov eax, x
};
}
如果这个程序使用eax
来增加i
的值,当我操作eax
时会发生什么?
编译器会保存 __asm
调用之前的寄存器并在执行 asm 代码后使用它们,还是会忽略 eax
被操纵并继续产生某种奇怪的行为?
eax 内部发生了什么?
编辑: 即使我的代码仅适用于 Visual C++,我也想知道通常会发生什么以及不同的编译器将如何处理。
简答:"it's your foot ... don't shoot it!"
如果您使用 asm{}
块,C/C++ 希望您知道自己在做什么。它不会为 "prepare for" 您的插入生成任何代码,也不会 "to clean up after it," 和 它 不会 知道或考虑你做了什么。在这种情况下,你会搬起石头砸自己的脚。你刚刚引入了一个错误,你自己制造的,通过做一些编译器没有预料到的,不知道的,并且可能会干扰编译器生成的代码 is 在做。
如果您打算在汇编代码中操作寄存器,则必须采取所有适当的步骤来保存和恢复这些寄存器的状态。您必须确切地知道自己在做什么。
What happens to eax internally?
内联汇编并不神奇或特殊。 C++ 编译器已经将 C++ 翻译成 asm。
您的内联 asm 刚刚包含在编译器生成的 asm 中。如果您想了解实际情况,请查看编译器的 asm 输出以了解您的代码如何适应。
即这部分问题的答案是它取决于编译器、上下文和优化选项,所以你应该只看生成的 asm 自己看看。
您的问题使用 MSVC 样式的内联汇编,saves/restores 围绕内联汇编注册(除了 ebp
,当然还有 esp
)。所以我认为你的例子可能总是没有效果。 MSVC 风格没有语法来向编译器传达有关寄存器使用的任何信息,或者在寄存器而不是内存中获取值 in/out。
您有时会看到 MSVC 内联汇编在没有 return
语句的 int
函数末尾的 eax
中留下一个值,在有限的情况下做出最安全的假设编译器不会在内联汇编结束和函数结束之间对 eax 做任何事情。
你在评论中说你想要 g++ 的答案,它甚至无法编译你的示例,但无论如何,我会为你写一个。
GNU C inline asm 使用不同的语法,它要求您告诉编译器哪些寄存器是输入(而不是修改),哪些是读写或只写的。还有哪些寄存器是被破坏的临时寄存器。
这取决于程序员使用约束向编译器正确描述 asm,否则你会踩到它的脚趾。 使用 GNU C 内联 asm 就像跳舞一样,您 可以 可能取得好的结果,但只有当您不小心时才会踩到编译器的脚趾。 (另外,通常编译器可以自己做出好的 asm,而内联 asm 是一种非常脆弱的优化方式;许多主要问题之一是内联后的持续传播不可能通过内联 asm。)
无论如何,让我们用一个 GNU C 内联 asm 的工作示例来尝试一下:
int foo_badasm(int n) {
int factorial = 1;
for (int i=1 ; i < n ; i++ ) {
// compile with -masm=intel, since I'm using Intel syntax here
asm volatile ("mov eax, %[x] # THIS LINE FROM INLINE ASM\n"
"# more lines\n"
// "xor eax,eax\n"
: // no outputs, making the volatile implicit even if we didn't specify it
: [x] "rmi" (factorial) // input can be reg, memory, or immediate
: // "eax" // uncomment this to tell the compiler we clobber eax, so our asm doesn't break step on the compiler's toes.
);
factorial *= i;
}
return factorial;
}
查看 the Godbolt compiler explorer 上带有 asm 输出的代码,以及没有 asm
语句的相同函数。
内部循环编译成这个(gcc 6.1 -O3 -fverbose-asm -masm=intel
,-fno-tree-vectorize
以保持简单)。你也可以用clang试试。
.L10:
mov eax, eax # THIS LINE FROM INLINE ASM # <retval>
imul eax, edx # <retval>, i
add edx, 1 # i,
cmp edi, edx # n, i
jne .L10 #,
ret
如您所见,在本例中,内联 asm 语句产生了一个空操作。 (mov eax,eax
将 rax
截断为 32 位,将高 32 位归零,但在此函数中它们已经为零。)
如果我们不做任何其他事情,比如将寄存器归零,或者 mov
来自不同的来源,我们就会破坏该功能。编译器生成的 asm 仅 依赖于 asm
语句中列出的约束,而不依赖于代码的文本(与 MSVC 不同)。
参见inline-assembly tag wiki for more info, especially this answer on the difference between MSVC and GNU C inline asm。
更重要的是,在实际使用内联 asm 之前阅读 https://gcc.gnu.org/wiki/DontUseInlineAsm。
恕我直言,最好的方法是不使用内联ASM,而是在ASM中写一个单独的函数。
通常的步骤是:
- 用 C 或 C++ 编写代码并使其正常运行。
- 打印出编译器生成的汇编列表。
- 以编译器生成的汇编代码为基础
(这使得编写调用和返回代码更容易)。
请注意,编写汇编语言来优化编译器生成的代码通常是在浪费开发时间。你应该在优化之前分析。
此外,编写内联汇编不可移植。
一些代码:
int x = 1;
for(int i = 1; i < 10; i++)
{
x *= i;
__asm {
mov eax, x
};
}
如果这个程序使用eax
来增加i
的值,当我操作eax
时会发生什么?
编译器会保存 __asm
调用之前的寄存器并在执行 asm 代码后使用它们,还是会忽略 eax
被操纵并继续产生某种奇怪的行为?
eax 内部发生了什么?
编辑: 即使我的代码仅适用于 Visual C++,我也想知道通常会发生什么以及不同的编译器将如何处理。
简答:"it's your foot ... don't shoot it!"
如果您使用 asm{}
块,C/C++ 希望您知道自己在做什么。它不会为 "prepare for" 您的插入生成任何代码,也不会 "to clean up after it," 和 它 不会 知道或考虑你做了什么。在这种情况下,你会搬起石头砸自己的脚。你刚刚引入了一个错误,你自己制造的,通过做一些编译器没有预料到的,不知道的,并且可能会干扰编译器生成的代码 is 在做。
如果您打算在汇编代码中操作寄存器,则必须采取所有适当的步骤来保存和恢复这些寄存器的状态。您必须确切地知道自己在做什么。
What happens to eax internally?
内联汇编并不神奇或特殊。 C++ 编译器已经将 C++ 翻译成 asm。
您的内联 asm 刚刚包含在编译器生成的 asm 中。如果您想了解实际情况,请查看编译器的 asm 输出以了解您的代码如何适应。
即这部分问题的答案是它取决于编译器、上下文和优化选项,所以你应该只看生成的 asm 自己看看。
您的问题使用 MSVC 样式的内联汇编,saves/restores 围绕内联汇编注册(除了 ebp
,当然还有 esp
)。所以我认为你的例子可能总是没有效果。 MSVC 风格没有语法来向编译器传达有关寄存器使用的任何信息,或者在寄存器而不是内存中获取值 in/out。
您有时会看到 MSVC 内联汇编在没有 return
语句的 int
函数末尾的 eax
中留下一个值,在有限的情况下做出最安全的假设编译器不会在内联汇编结束和函数结束之间对 eax 做任何事情。
你在评论中说你想要 g++ 的答案,它甚至无法编译你的示例,但无论如何,我会为你写一个。
GNU C inline asm 使用不同的语法,它要求您告诉编译器哪些寄存器是输入(而不是修改),哪些是读写或只写的。还有哪些寄存器是被破坏的临时寄存器。
这取决于程序员使用约束向编译器正确描述 asm,否则你会踩到它的脚趾。 使用 GNU C 内联 asm 就像跳舞一样,您 可以 可能取得好的结果,但只有当您不小心时才会踩到编译器的脚趾。 (另外,通常编译器可以自己做出好的 asm,而内联 asm 是一种非常脆弱的优化方式;许多主要问题之一是内联后的持续传播不可能通过内联 asm。)
无论如何,让我们用一个 GNU C 内联 asm 的工作示例来尝试一下:
int foo_badasm(int n) {
int factorial = 1;
for (int i=1 ; i < n ; i++ ) {
// compile with -masm=intel, since I'm using Intel syntax here
asm volatile ("mov eax, %[x] # THIS LINE FROM INLINE ASM\n"
"# more lines\n"
// "xor eax,eax\n"
: // no outputs, making the volatile implicit even if we didn't specify it
: [x] "rmi" (factorial) // input can be reg, memory, or immediate
: // "eax" // uncomment this to tell the compiler we clobber eax, so our asm doesn't break step on the compiler's toes.
);
factorial *= i;
}
return factorial;
}
查看 the Godbolt compiler explorer 上带有 asm 输出的代码,以及没有 asm
语句的相同函数。
内部循环编译成这个(gcc 6.1 -O3 -fverbose-asm -masm=intel
,-fno-tree-vectorize
以保持简单)。你也可以用clang试试。
.L10:
mov eax, eax # THIS LINE FROM INLINE ASM # <retval>
imul eax, edx # <retval>, i
add edx, 1 # i,
cmp edi, edx # n, i
jne .L10 #,
ret
如您所见,在本例中,内联 asm 语句产生了一个空操作。 (mov eax,eax
将 rax
截断为 32 位,将高 32 位归零,但在此函数中它们已经为零。)
如果我们不做任何其他事情,比如将寄存器归零,或者 mov
来自不同的来源,我们就会破坏该功能。编译器生成的 asm 仅 依赖于 asm
语句中列出的约束,而不依赖于代码的文本(与 MSVC 不同)。
参见inline-assembly tag wiki for more info, especially this answer on the difference between MSVC and GNU C inline asm。
更重要的是,在实际使用内联 asm 之前阅读 https://gcc.gnu.org/wiki/DontUseInlineAsm。
恕我直言,最好的方法是不使用内联ASM,而是在ASM中写一个单独的函数。
通常的步骤是:
- 用 C 或 C++ 编写代码并使其正常运行。
- 打印出编译器生成的汇编列表。
- 以编译器生成的汇编代码为基础 (这使得编写调用和返回代码更容易)。
请注意,编写汇编语言来优化编译器生成的代码通常是在浪费开发时间。你应该在优化之前分析。
此外,编写内联汇编不可移植。