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 做你想做的,如果你使用 mallocnew 指向 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 名称。)