C语言 x86 - 如何在内联程序集中使用 for 循环将值插入数组



我被分配的一个练习是将数组的值从 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;
}

使用int numbers[10];给你的 asm 一个它期望的数组,而不是一个指针。

并且通常不会在内联 asm 中弄乱esp。 或者,如果这样做,请确保esp在内联 asm 块的末尾具有与开始时相同的值。您可能无缘无故地将这些add esp,4输入在那里,但前提是编译器在拆除堆栈帧时用leavemov esp,ebp丢弃旧的esp值。删除所有add esp,4,它们不应该在那里。 (请参阅此答案的底部,了解仅执行必要操作的简化循环。


您正在破坏指针值旁边的堆栈内存,因此当函数尝试返回时,您可能会崩溃。 (使用调试器查看哪些指令错误)。 您已经用一个小整数覆盖了返回地址,因此,如果我正确分析了这一点,则从未映射的地址获取代码会导致页面错误。

在C语言中,数组和指针对[]使用相同的语法,但它们并不相同。 使用指针,编译器需要将指针值获取到寄存器中,然后相对于该进行索引。 (在内联 asm 中,您必须自己执行此操作,但您的代码不需要。 对于数组,索引相对于数组基址,编译器始终知道在哪里可以找到数组(堆栈上的自动存储或静态存储)。

我正在简化一点:结构可以包含一个数组,在这种情况下,它是一个正确的数组类型,不会"衰减"到指针。(相关:根据AMD64 ABI的数组是哪种C11数据类型)。 因此,foo->arr[9]将是没有静态或自动存储的实际数组的引用,因此编译器不一定"已经"免费拥有基址。

请注意,即使是声明为int foo(int arr[10])的函数 arg 实际上也是一个指针,而不是数组。sizeof(arr)4(在 x86 上使用 32 位指针),这与在函数内将其声明为局部变量不同。


这种差异在装配中很重要。仅当numbers是数组类型而不是指针类型时,mov numbers[TYPE numbers * eax], eax才会执行所需的操作。你的 asm 相当于(&numbers)[index] = (int*)index;,而不是numbers[index] = index;。 这就是您覆盖堆栈上靠近指针值的位置的其他内容的方式。

在MSVC内联asm中,局部变量名被组装为[ebp+constant],所以当numbers是一个数组时,它的元素从numbers开始在堆栈上。 但是,当numbers是指针时,指针位于该位置的堆栈上。
如果您使用mallocnewnumbers指向某些动态分配的存储,则必须mov edx, numbers/mov [edx + eax*TYPE numbers], eax才能执行所需的操作。

即 MSVC不会神奇地使 asm 语法像 C 指针语法那样工作,并且无法有效地做到这一点,因为它需要一个额外的寄存器(您的代码可能正在使用该寄存器)。 您(无意中)编写了覆盖堆栈上的指针值的 asm,然后覆盖了该值之上的另外 9 个 DWORD。 这是你可以用内联 asm 做的事情,所以你的代码编译时没有警告。


如果您numbers未初始化,那么(使用正确的指针取消引用)您的代码几乎肯定会崩溃,原因与编译器生成的int *numbers; numbers[0] = 0;代码相同。 所以是的,保罗的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];vs.int *numbers;,所有这些都没有优化选项,因此他们不会优化任何东西。 来自内联 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 语法本身的错误。 (或者查看编译器在代码之前/之后生成什么代码)。 请注意,MSVC 的"asm 输出"并不总是与它放入目标文件中的机器代码匹配,这与 gcc 或 clang 不同。 为了真正确定,反汇编目标文件或可执行文件。 (但是,您通常会丢失符号名称,因此同时查看两者可能会有所帮助。


顺便说一句,使用内联 asm 并不是首先学习 asm 的最简单方法。 MSVC 内联 asm 还可以(不像 GNU C 内联 asm 语法,在 GNU C 内联 asm 语法中,您需要了解 asm 和编译器才能正确地向编译器描述您的 asm),但不是很好,并且有严重的疣。 用纯 asm 编写整个函数并从 C 调用它们是我推荐的学习方法。

我还强烈建议只阅读针对小函数的优化编译器输出,以了解如何在 asm 中执行各种操作。 参见Matt Godbolt的CppCon2017演讲:"我的编译器最近为我做了什么?打开编译器的盖子"。


顺便说一句,这是我编写函数的方式(如果我必须使用 MSVC 内联 asm (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
}
}

请注意,唯一的存储位于数组中,并且没有加载。 循环中的指令要少得多。 在这种情况下(与通常不同),MSVC 有限的asm语法实际上并没有为将数据传入/移出asm块带来任何开销,但它仍然不比从纯 C 循环的优化编译器输出中获得的更好。 (当然,除非数组volatile,否则循环会优化,如果你的函数返回而不对它做任何事情。

如果要让 C 变量在循环末尾保存indexmov index, eax循环外部。 因此,从逻辑上讲,index存在于循环内部的eax中,之后仅存储到内存中。 MSVC 语法提供了一种黑客方法,可以将一个值返回给 C,而无需将其存储到编译器必须重新加载它的内存中:将值保留在 asm 块中的eax,位于没有return语句的非void函数末尾。 显然,MSVC "理解"了这一点,即使在内联此类函数时也能使其工作。 但这仅适用于一个标量值。

启用优化后,mov numbers[4*eax], eax可以编译为mov [esp+constant + 4*eax], eax,即相对于 ESP 而不是 EBP。 或者可能不是,如果 MSVC 总是在使用内联 asm 的函数中创建堆栈帧,则 IDK。 或者,如果numbers是一个静态数组,它将只是一个绝对地址(即链接时间常量),因此在asm中它仍然只是实际的交易品种名称_numbers。 (因为 Windows 在 C 名称前面附加了一个前导_

自从我写汇编以来已经有很长时间(20+ 年)了,但不是需要分配数字数组吗?(也可以将 Inc 移动到更清洁的地方)

#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 年的事情!

最新更新