使用 int numbers[10]; 为您的 asm 提供一个预期的数组,而不是指针。
通常不要在 inline asm 中使用esp。或者,如果您这样做,请确保 esp 在 inline-asm 块末尾的值与在开始时的值相同。您可能已经无缘无故地将那些add esp,4 insns 放在那里,但前提是编译器将旧的esp 值与leave 或mov esp,ebp 作为一部分拆除堆栈框架。 删除你所有的add esp,4,他们不应该在那里。 (有关仅执行必要操作的简化循环,请参阅此答案的底部。)
您正在破坏指针值旁边的堆栈内存,因此当函数尝试返回时您可能会崩溃。 (使用调试器查看哪些指令出错)。你已经用一个小整数覆盖了返回地址,所以如果我分析正确的话,从一个未映射的地址提取代码会导致页面错误。
在 C 中,数组和指针对 [] 使用相同的语法,但它们不相同。使用指针,编译器需要将 指针值 放入寄存器,然后相对于该寄存器进行索引。 (并且在 inline asm 中,您必须自己执行此操作,但您的代码不需要。)对于数组,索引相对于 数组基址,并且编译器总是知道在哪里可以找到数组(自动存储在堆栈上,或者静态存储中)。
我正在简化一点:结构可以包含一个数组,在这种情况下,它是一个正确的数组类型,不会“衰减”为指针。 (相关:What kind of C11 data type is an array according to the AMD64 ABI)。所以foo->arr[9] 将是一个没有静态或自动存储的实际数组的 deref,因此编译器不一定“已经”免费拥有基地址。
请注意,即使是声明为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 是一个指针时,指针 就在那个位置的堆栈上。如果您使用malloc 或new 将numbers 指向某个动态分配的存储,则您必须
mov edx, numbers / mov [edx + eax*TYPE numbers], eax 做您想做的事情。
即MSVC 确实 not 神奇地使 asm 语法像 C 指针语法一样工作,并且不能有效地这样做,因为它需要一个额外的寄存器(您的代码可能正在使用它)。您(无意中)编写了覆盖堆栈上指针值的 asm,然后覆盖了上面的另外 9 个 DWORD。这就是您可以使用内联 asm 执行的操作,因此您的代码编译时不会出现警告。
如果您未初始化 numbers,那么(使用适当的指针取消引用)您的代码几乎肯定会崩溃,原因与编译器为 int *numbers; numbers[0] = 0; 生成的代码相同。所以是的,Paul 的 C++ new 答案部分正确,并修复了 C 错误,但没有修复 asm(缺少)指针取消引用错误。如果这使它不会崩溃,那是因为编译器在调用 new 之前保留了更多的堆栈空间,并且恰好足以让您在堆栈内存上乱涂乱画而不会破坏返回地址或其他东西。
我尝试在 Godbolt 编译器资源管理器上查看 MSVC CL19 中的 asm,但该编译器版本(带有默认选项)仅保留了几个带有 int *numbers = new int[10]; 的 DWORD,没有足够的空间用于代码以避免破坏返回地址在&numbers以上写入内存时。大概您使用的任何编译器/版本/选项都会发出不同的代码,从而保留更多的堆栈空间,从而避免崩溃,因为您接受了该答案。
请参阅 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$3:
cmp DWORD PTR _index$[ebp], 9
jge SHORT $$END_LOOP$4
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$3
$$END_LOOP$4:
您可能认为您已经在编写 asm,但是查看编译器的实际 asm 输出可以帮助您发现使用 asm 语法本身的错误。 (或查看编译器在您的代码之前/之后生成的代码)。请注意,与 gcc 或 clang 不同,MSVC 的“asm 输出”并不总是与它放入目标文件的机器代码匹配。可以肯定的是,反汇编目标文件或可执行文件。 (但你大多会丢失符号名称,因此查看两者会有所帮助。)
顺便说一句,首先使用内联 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 内联 asm (https://gcc.gnu.org/wiki/DontUseInlineAsm),并且我不想使用 SSE2 或 AVX2 SIMD 展开或优化...)。
我将数组索引保存在eax 中,从不将其溢出到内存中。此外,我将循环重组为 do{}while() 循环,因为这在 asm 中更自然、更高效和惯用。见Why are loops always compiled like this?。
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 块施加任何开销,但它仍然不比优化编译器输出所得到的更好对于纯 C 循环。 (当然循环会优化掉,除非数组是volatile,如果你的函数没有做任何事情就返回了。)
如果您想在循环结束时有一个 C 变量保存 index,则在循环外使用 mov index, eax。所以逻辑上index 存在于循环内的eax 中,之后才存储到内存中。 MSVC 语法提供了一种将一个值返回给 C 而不将其存储到编译器必须重新加载它的内存的 hacky 方法:将eax 中的值留在非void 函数末尾的 asm 块中,没有return 声明。显然,MSVC“理解”了这一点,并且即使在内联这样的函数时也能正常工作。但这仅适用于单个标量值。
启用优化后,mov numbers[4*eax], eax 可以编译为mov [esp+constant + 4*eax], eax,即相对于 ESP 而不是 EBP。或者也许不是,如果 MSVC 总是在使用内联汇编的函数中创建堆栈帧,IDK。或者如果numbers 是一个静态数组,它只是一个绝对地址(即链接时间常数),所以在 asm 中它仍然只是实际的符号名称_numbers。 (因为 Windows 会在 C 名称前添加前导 _。)