【问题标题】:x86 - How to insert values into an array using for loops in Inline Assemblyx86 - 如何使用内联汇编中的 for 循环将值插入数组
【发布时间】:2018-07-07 13:36:13
【问题描述】:

分配给我的一个练习是将数组的值设置为 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;
}

【问题讨论】:

标签: c arrays visual-c++ x86 inline-assembly


【解决方案1】:

使用 int numbers[10]; 为您的 asm 提供一个预期的数组,而不是指针。

通常不要在 inline asm 中使用esp。或者,如果您这样做,请确保 esp 在 inline-asm 块末尾的值与在开始时的值相同。您可能已经无缘无故地将那些add esp,4 insns 放在那里,但前提是编译器将旧的esp 值与leavemov esp,ebp 作为一部分拆除堆栈框架。 删除你所有的add esp,4,他们不应该在那里。 (有关仅执行必要操作的简化循环,请参阅此答案的底部。)


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

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

我正在简化一点:结构可以包含一个数组,在这种情况下,它是一个正确的数组类型,不会“衰减”为指针。 (相关:What kind of C11 data type is an array according to the AMD64 ABI)。所以foo-&gt;arr[9] 将是一个没有静态或自动存储的实际数组的 deref,因此编译器不一定“已经”免费拥有基地址。

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


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

在 MSVC inline-asm 中,局部变量名被组装成[ebp+constant],所以当numbers 是一个数组时,它的元素在堆栈上从numbers 开始。但是当numbers 是一个指针时,指针 就在那个位置的堆栈上。如果您使用mallocnewnumbers 指向某个动态分配的存储,则您必须
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,没有足够的空间用于代码以避免破坏返回地址在&amp;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 名称前添加前导 _。)

【讨论】:

    【解决方案2】:

    自从我编写程序集以来已经有很长一段时间(20 多年)了,但是您不是需要分配 numbers 数组吗? (也可以把公司搬到更干净的地方)

    #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;
    }
    

    注意:不知道为什么你需要移动堆栈指针(esp),但很高兴承认我已经忘记了 20 年!

    【讨论】:

    • 感谢您的回复 :) 至于堆栈指针的第一个实例,我一直在尝试参考之前练习中的代码,因此可能会有额外的不必要说明。但是您的解决方案有效!
    • @PeterCordes 我才开始学习汇编,所以如果我不是专家,我很抱歉,就像我原来的帖子所说的那样。从那以后我清理了我的代码,它工作得很好,但没有必要对我如此苛刻。我只是一个初学者。
    • @TobyCook 不要把它放在心上。不幸的是,CS 行业(尤其是 SO)在诋毁 cmets 而不是支持和指导上蓬勃发展......我更喜欢后者,但当你遇到前者时不要感到惊讶
    • @TobyCook:无论如何,我认为对可能会混淆或误导未来读者的错误代码进行严厉批评是很重要的。请记住,被“攻击”的是代码,而不是人。我将其表述为“这真的有效吗”,因为我不使用 MSVC 或者它是内联 asm 语法;如果我确定这是错误的,我会直接编辑。再次抱歉冒犯了我的措辞;希望这可以帮助您弄清楚代码在做什么。 (顺便说一句,尝试查看函数的反汇编,包括编译器生成的 asm 和您的 inline-asm 变成的反汇编。)
    • 哇@PeterCordes 写得真棒,干得好!我会投赞成票,因为我绝对认为这是一个值得接受的答案的出色回应......干得好! :)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-09-17
    相关资源
    最近更新 更多