x86 - 如何在内联汇编中使用 for 循环将值插入数组
x86 - How to insert values into an array using for loops in Inline Assembly
分配给我的练习之一是将数组的值设置为 0 到 9,并使用尽可能多的内联汇编打印总和。我对内联汇编没有什么经验,并且已经用尽了我的研究来寻找解决方案。
这是我目前的代码。它编译没有错误,但是,当我尝试 运行 应用程序时,它只是崩溃了。我知道它不完整,但我不确定我的逻辑是否正确。
#include <stdio.h>
int main(int argc, char **argv)
{
int *numbers;
int index;
__asm
{
// Set index value to 0
mov index, 0
// Jump to check
jmp $CHECK_index
// Increment the value of index by 1
$INCREMENT_index:
inc index
// Check if index >= 9
$CHECK_index:
cmp index, 9
jge $END_LOOP
// Move index value to eax register
mov eax, index
// Move value of eax register to array
mov numbers[TYPE numbers * eax], eax
// Clean the stack
add esp, 4
jmp $INCREMENT_index
$END_LOOP:
add esp, 4
}
return 0;
}
自从我写汇编以来已经有很长一段时间(20+年)了,但是你不是需要分配数字数组吗? (也可以将公司移到更干净的地方)
#include <stdio.h>
int main(int argc, char **argv)
{
int *numbers = new int[10]; // <--- Missing allocate
int index;
__asm
{
// Set index value to 0
mov index, 0
// Check if index >= 9
$CHECK_index:
cmp index, 9
jge $END_LOOP
// Move index value to eax register
mov eax, index
// Move value of eax register to array
mov numbers[TYPE numbers * eax], eax
// Increment the value of index by 1
inc index // <---- inc is cleaner here
jmp $CHECK_index
$END_LOOP:
}
return 0;
}
注意:不确定您为什么需要移动堆栈指针(尤其是),但很高兴承认我已经忘记了 20 年的事情!
使用 int numbers[10];
为您的 asm 提供它所期望的数组,而不是指针。
并且一般来说 不要乱用内联 asm 中的 esp
。或者,如果这样做,请确保 esp
在 inline-asm 块末尾的值与开始时的值相同。你 可能 无缘无故地拥有那些 add esp,4
insn,但前提是编译器用 [=17 丢弃旧的 esp
值=] 或 mov esp,ebp
作为拆除堆栈框架的一部分。 删除你所有的 add esp,4
,它们不应该在那里。 (请参阅此答案的底部,了解仅执行必要操作的简化循环。)
您正在破坏指针值旁边的堆栈内存,因此当函数尝试 return 时您可能会崩溃。 (使用调试器 查看哪个指令出错)。您已经用一个小整数覆盖了 return 地址,因此如果我分析正确的话,从未映射的地址获取代码会导致页面错误。
在 C 中,数组和指针对 []
使用相同的语法,但它们 与 不同 。对于指针,编译器需要将 指针值 放入寄存器中,然后对其进行索引。 (在内联 asm 中,您必须自己执行此操作,但您的代码 不会 。)对于数组,索引是相对于 数组基地址,编译器总是知道在哪里找到数组(自动存储在堆栈上,或静态存储)。
我稍微简化了一点:一个结构可以包含一个数组,在这种情况下,它是一个不 "decay" 指向指针的正确数组类型。 (相关:)。所以 foo->arr[9]
将是一个没有静态或自动存储的实际数组的取消引用,因此编译器不一定 "already" 有免费的基地址。
请注意,即使声明为 int foo(int arr[10])
的函数 arg 实际上也是一个指针,而不是 数组。 sizeof(arr)
是 4
(在 x86 上使用 32 位指针),这与您将其声明为函数内部的局部变量不同。
这个区别在汇编中很重要。 mov numbers[TYPE numbers * eax], eax
只有在 numbers
是数组类型而不是指针类型时才做你想做的事情。 你的 asm 相当于 (&numbers)[index] = (int*)index;
,而不是 numbers[index] = index;
。这就是您在存储指针值的位置附近覆盖堆栈上其他内容的方式。
在 MSVC inline-asm 中,局部变量名被组装为 [ebp+constant]
,因此当 numbers
是一个数组时,其元素在堆栈上从 numbers
开始。但是当 numbers
是一个指针时,pointer 在栈上那个位置。你必须
mov edx, numbers
/ mov [edx + eax*TYPE numbers], eax
做你想做的,如果你使用 malloc
或 new
指向 numbers
一些动态分配的存储。
即MSVC 确实 not 神奇地使 asm 语法像 C 指针语法一样工作,并且不能有效地这样做,因为它需要一个额外的寄存器(您的代码可能正在使用它)。您(无意中)编写了 asm 来覆盖堆栈上的指针值,然后覆盖其上方的另外 9 个 DWORD。这是你可以用内联汇编做的事情,所以你的代码编译时没有警告。
如果您让 numbers
未初始化,那么(通过适当的指针取消引用)您的代码几乎肯定会崩溃,原因与编译器为 int *numbers; numbers[0] = 0;
生成的代码一样。所以是的,Paul 的 C++ new
答案部分正确并修复了 C 错误,但没有修复 asm(缺少)指针取消引用错误。如果这使得它没有崩溃,那是因为编译器在调用 new
之前保留了更多的堆栈 space,这恰好足以让你在堆栈内存上涂写而不破坏 return 地址, 或者什么的。
我试着在 Godbolt 编译器资源管理器上查看 MSVC CL19 的 asm,但是该编译器版本(具有默认选项)只保留了几个 int *numbers = new int[10];
的 DWORD,不够 space您的代码避免在 &numbers
之上写入内存时破坏 return 地址。大概您使用的任何编译器/版本/选项都会发出不同的代码,这些代码会保留更多堆栈 space 从而避免崩溃,因为您接受了那个答案。
请参阅 Godbolt 编译器资源管理器上的 source + asm,对于 int numbers[10];
与 int *numbers = new int[10];
与 int *numbers;
,都没有优化选项,因此它们不会优化任何东西离开。 inline-asm 块中的代码在所有情况下都是相同的,除了 _numbers$ = -12
之类的数字常量,编译器将其用作 ebp
的偏移量以寻址局部变量:
;; from the int *numbers = new int[10]; version:
_numbers$ = -12 ; size = 4
$T1 = -8 ; size = 4
_index$ = -4 ; size = 4
mov DWORD PTR _index$[ebp], 0
$$CHECK_index:
cmp DWORD PTR _index$[ebp], 9
jge SHORT $$END_LOOP
mov eax, DWORD PTR _index$[ebp]
mov DWORD PTR _numbers$[ebp+eax*4], eax ; this is [ebp-12 + eax*4]
inc DWORD PTR _index$[ebp]
jmp SHORT $$CHECK_index
$$END_LOOP:
您可能认为您已经在使用 asm 编写,但查看编译器的实际 asm 输出可以帮助您发现使用 asm 语法本身的错误。 (或者查看编译器在您的代码之前/之后生成的代码)。请注意,与 gcc 或 clang 不同,MSVC 的 "asm output" 并不总是与它放入目标文件的机器代码相匹配。确实,反汇编目标文件或可执行文件。 (但这样一来,您大多会丢失符号名称,因此同时查看两者会有所帮助。)
顺便说一句,使用内联 asm 并不是学习 asm 最简单的方法。 MSVC 内联 asm 有点不错(不像 GNU C 内联 asm 语法,你需要了解 asm 和编译器才能正确地向编译器描述你的 asm),但不是很好,并且有严重的缺陷。用纯 asm 编写整个函数并从 C 调用它们是我推荐的学习方法。
我还强烈建议 阅读 优化编译器输出 tiny 函数,看看如何在 asm 中做各种事情。请参阅 Matt Godbolt 的 CppCon2017 演讲:“What Has My Compiler Done for Me Lately? Unbolting the Compiler's Lid”.
顺便说一句,这是我编写函数的方式(如果我必须使用 MSVC 内联汇编 (https://gcc.gnu.org/wiki/DontUseInlineAsm),并且我不想展开或使用 SSE2 或 AVX2 SIMD 进行优化...)。
我将数组索引保存在 eax
中,从不将其溢出到内存中。此外,我将循环重组为 do{}while()
循环,因为这在 asm 中更加自然、高效和惯用。参见 。
void clean_version(void)
{
int numbers[10];
__asm
{
// index lives in eax
xor eax,eax // index = 0
// The loop always runs at least once, so no check is needed before falling into the first iteration
$store_loop: // do {
// store index into the array
mov numbers[TYPE numbers * eax], eax
// Increment the value of index by 1
inc eax
cmp eax, 9 // } while(index<=9);
jle $store_loop
}
}
请注意,唯一的存储在数组中,没有加载。循环中的指令要少得多。在这种情况下 (unlike usual),MSVC 有限的 asm
语法实际上并没有对将数据输入/输出 asm
块施加任何开销,但它仍然不比你的更好d 从纯 C 循环的优化编译器输出中获取。 (当然,除非数组是 volatile
,如果你的函数 returns 没有对它做任何事情,循环会优化掉。)
如果你想让一个 C 变量在循环结束时保持 index
,在循环外保持 mov index, eax
。所以逻辑上 index
存在于循环内的 eax
中,并且仅在之后存储到内存中。 MSVC 语法提供了一种骇人听闻的方式来将 return 一个值传递给 C,而无需将其存储到编译器必须重新加载它的内存中:将 eax
中的值保留在非void
函数没有 return
语句。显然 MSVC "understands" this 并使它即使在内联这样的函数时也能工作。但这仅适用于一个标量值。
启用优化后,mov numbers[4*eax], eax
可能会编译为 mov [esp+constant + 4*eax], eax
,即相对于 ESP 而不是 EBP。或者可能不是,IDK 如果 MSVC 总是在使用内联 asm 的函数中创建堆栈帧。或者如果 numbers
是一个静态数组,它只是一个绝对地址(即 link 时间常数),所以在 asm 中它仍然只是实际的符号名称 _numbers
. (因为 Windows 将前导 _
添加到 C 名称。)
分配给我的练习之一是将数组的值设置为 0 到 9,并使用尽可能多的内联汇编打印总和。我对内联汇编没有什么经验,并且已经用尽了我的研究来寻找解决方案。
这是我目前的代码。它编译没有错误,但是,当我尝试 运行 应用程序时,它只是崩溃了。我知道它不完整,但我不确定我的逻辑是否正确。
#include <stdio.h>
int main(int argc, char **argv)
{
int *numbers;
int index;
__asm
{
// Set index value to 0
mov index, 0
// Jump to check
jmp $CHECK_index
// Increment the value of index by 1
$INCREMENT_index:
inc index
// Check if index >= 9
$CHECK_index:
cmp index, 9
jge $END_LOOP
// Move index value to eax register
mov eax, index
// Move value of eax register to array
mov numbers[TYPE numbers * eax], eax
// Clean the stack
add esp, 4
jmp $INCREMENT_index
$END_LOOP:
add esp, 4
}
return 0;
}
自从我写汇编以来已经有很长一段时间(20+年)了,但是你不是需要分配数字数组吗? (也可以将公司移到更干净的地方)
#include <stdio.h>
int main(int argc, char **argv)
{
int *numbers = new int[10]; // <--- Missing allocate
int index;
__asm
{
// Set index value to 0
mov index, 0
// Check if index >= 9
$CHECK_index:
cmp index, 9
jge $END_LOOP
// Move index value to eax register
mov eax, index
// Move value of eax register to array
mov numbers[TYPE numbers * eax], eax
// Increment the value of index by 1
inc index // <---- inc is cleaner here
jmp $CHECK_index
$END_LOOP:
}
return 0;
}
注意:不确定您为什么需要移动堆栈指针(尤其是),但很高兴承认我已经忘记了 20 年的事情!
使用 int numbers[10];
为您的 asm 提供它所期望的数组,而不是指针。
并且一般来说 不要乱用内联 asm 中的 esp
。或者,如果这样做,请确保 esp
在 inline-asm 块末尾的值与开始时的值相同。你 可能 无缘无故地拥有那些 add esp,4
insn,但前提是编译器用 [=17 丢弃旧的 esp
值=] 或 mov esp,ebp
作为拆除堆栈框架的一部分。 删除你所有的 add esp,4
,它们不应该在那里。 (请参阅此答案的底部,了解仅执行必要操作的简化循环。)
您正在破坏指针值旁边的堆栈内存,因此当函数尝试 return 时您可能会崩溃。 (使用调试器 查看哪个指令出错)。您已经用一个小整数覆盖了 return 地址,因此如果我分析正确的话,从未映射的地址获取代码会导致页面错误。
在 C 中,数组和指针对 []
使用相同的语法,但它们 与 不同 。对于指针,编译器需要将 指针值 放入寄存器中,然后对其进行索引。 (在内联 asm 中,您必须自己执行此操作,但您的代码 不会 。)对于数组,索引是相对于 数组基地址,编译器总是知道在哪里找到数组(自动存储在堆栈上,或静态存储)。
我稍微简化了一点:一个结构可以包含一个数组,在这种情况下,它是一个不 "decay" 指向指针的正确数组类型。 (相关:foo->arr[9]
将是一个没有静态或自动存储的实际数组的取消引用,因此编译器不一定 "already" 有免费的基地址。
请注意,即使声明为 int foo(int arr[10])
的函数 arg 实际上也是一个指针,而不是 数组。 sizeof(arr)
是 4
(在 x86 上使用 32 位指针),这与您将其声明为函数内部的局部变量不同。
这个区别在汇编中很重要。 mov numbers[TYPE numbers * eax], eax
只有在 numbers
是数组类型而不是指针类型时才做你想做的事情。 你的 asm 相当于 (&numbers)[index] = (int*)index;
,而不是 numbers[index] = index;
。这就是您在存储指针值的位置附近覆盖堆栈上其他内容的方式。
在 MSVC inline-asm 中,局部变量名被组装为 [ebp+constant]
,因此当 numbers
是一个数组时,其元素在堆栈上从 numbers
开始。但是当 numbers
是一个指针时,pointer 在栈上那个位置。你必须
mov edx, numbers
/ mov [edx + eax*TYPE numbers], eax
做你想做的,如果你使用 malloc
或 new
指向 numbers
一些动态分配的存储。
即MSVC 确实 not 神奇地使 asm 语法像 C 指针语法一样工作,并且不能有效地这样做,因为它需要一个额外的寄存器(您的代码可能正在使用它)。您(无意中)编写了 asm 来覆盖堆栈上的指针值,然后覆盖其上方的另外 9 个 DWORD。这是你可以用内联汇编做的事情,所以你的代码编译时没有警告。
如果您让 numbers
未初始化,那么(通过适当的指针取消引用)您的代码几乎肯定会崩溃,原因与编译器为 int *numbers; numbers[0] = 0;
生成的代码一样。所以是的,Paul 的 C++ new
答案部分正确并修复了 C 错误,但没有修复 asm(缺少)指针取消引用错误。如果这使得它没有崩溃,那是因为编译器在调用 new
之前保留了更多的堆栈 space,这恰好足以让你在堆栈内存上涂写而不破坏 return 地址, 或者什么的。
我试着在 Godbolt 编译器资源管理器上查看 MSVC CL19 的 asm,但是该编译器版本(具有默认选项)只保留了几个 int *numbers = new int[10];
的 DWORD,不够 space您的代码避免在 &numbers
之上写入内存时破坏 return 地址。大概您使用的任何编译器/版本/选项都会发出不同的代码,这些代码会保留更多堆栈 space 从而避免崩溃,因为您接受了那个答案。
请参阅 Godbolt 编译器资源管理器上的 source + asm,对于 int numbers[10];
与 int *numbers = new int[10];
与 int *numbers;
,都没有优化选项,因此它们不会优化任何东西离开。 inline-asm 块中的代码在所有情况下都是相同的,除了 _numbers$ = -12
之类的数字常量,编译器将其用作 ebp
的偏移量以寻址局部变量:
;; from the int *numbers = new int[10]; version:
_numbers$ = -12 ; size = 4
$T1 = -8 ; size = 4
_index$ = -4 ; size = 4
mov DWORD PTR _index$[ebp], 0
$$CHECK_index:
cmp DWORD PTR _index$[ebp], 9
jge SHORT $$END_LOOP
mov eax, DWORD PTR _index$[ebp]
mov DWORD PTR _numbers$[ebp+eax*4], eax ; this is [ebp-12 + eax*4]
inc DWORD PTR _index$[ebp]
jmp SHORT $$CHECK_index
$$END_LOOP:
您可能认为您已经在使用 asm 编写,但查看编译器的实际 asm 输出可以帮助您发现使用 asm 语法本身的错误。 (或者查看编译器在您的代码之前/之后生成的代码)。请注意,与 gcc 或 clang 不同,MSVC 的 "asm output" 并不总是与它放入目标文件的机器代码相匹配。确实,反汇编目标文件或可执行文件。 (但这样一来,您大多会丢失符号名称,因此同时查看两者会有所帮助。)
顺便说一句,使用内联 asm 并不是学习 asm 最简单的方法。 MSVC 内联 asm 有点不错(不像 GNU C 内联 asm 语法,你需要了解 asm 和编译器才能正确地向编译器描述你的 asm),但不是很好,并且有严重的缺陷。用纯 asm 编写整个函数并从 C 调用它们是我推荐的学习方法。
我还强烈建议 阅读 优化编译器输出 tiny 函数,看看如何在 asm 中做各种事情。请参阅 Matt Godbolt 的 CppCon2017 演讲:“What Has My Compiler Done for Me Lately? Unbolting the Compiler's Lid”.
顺便说一句,这是我编写函数的方式(如果我必须使用 MSVC 内联汇编 (https://gcc.gnu.org/wiki/DontUseInlineAsm),并且我不想展开或使用 SSE2 或 AVX2 SIMD 进行优化...)。
我将数组索引保存在 eax
中,从不将其溢出到内存中。此外,我将循环重组为 do{}while()
循环,因为这在 asm 中更加自然、高效和惯用。参见
void clean_version(void)
{
int numbers[10];
__asm
{
// index lives in eax
xor eax,eax // index = 0
// The loop always runs at least once, so no check is needed before falling into the first iteration
$store_loop: // do {
// store index into the array
mov numbers[TYPE numbers * eax], eax
// Increment the value of index by 1
inc eax
cmp eax, 9 // } while(index<=9);
jle $store_loop
}
}
请注意,唯一的存储在数组中,没有加载。循环中的指令要少得多。在这种情况下 (unlike usual),MSVC 有限的 asm
语法实际上并没有对将数据输入/输出 asm
块施加任何开销,但它仍然不比你的更好d 从纯 C 循环的优化编译器输出中获取。 (当然,除非数组是 volatile
,如果你的函数 returns 没有对它做任何事情,循环会优化掉。)
如果你想让一个 C 变量在循环结束时保持 index
,在循环外保持 mov index, eax
。所以逻辑上 index
存在于循环内的 eax
中,并且仅在之后存储到内存中。 MSVC 语法提供了一种骇人听闻的方式来将 return 一个值传递给 C,而无需将其存储到编译器必须重新加载它的内存中:将 eax
中的值保留在非void
函数没有 return
语句。显然 MSVC "understands" this 并使它即使在内联这样的函数时也能工作。但这仅适用于一个标量值。
启用优化后,mov numbers[4*eax], eax
可能会编译为 mov [esp+constant + 4*eax], eax
,即相对于 ESP 而不是 EBP。或者可能不是,IDK 如果 MSVC 总是在使用内联 asm 的函数中创建堆栈帧。或者如果 numbers
是一个静态数组,它只是一个绝对地址(即 link 时间常数),所以在 asm 中它仍然只是实际的符号名称 _numbers
. (因为 Windows 将前导 _
添加到 C 名称。)