【问题标题】:What happens at assembly level when you have functions with large inputs当您的函数具有大量输入时,在汇编级别会发生什么
【发布时间】:2018-05-02 18:54:10
【问题描述】:

虽然 JavaScript 不直接变成汇编,但它应该演示一个普遍的问题,即如果在汇编中实现高级函数会是什么样子如果函数的输入很大。比如说这个案例:

myfunc(1, 2, 3)

那里的变量是小整数,因此它们可以放在单独的寄存器中。但是假设你有:

var a = 'some markdown readme...'
myfunc('my really long string', a, 'etc.')

想知道如何在汇编中做到这一点(在高层次上)。

似乎不会使用程序集调用堆栈来存储这些值,因为它们很大。也许它存储内存地址和它的偏移量(但如果它是动态的......)。很想知道这是如何工作的。

【问题讨论】:

  • 字符串需要可变数量的存储空间,因此它们始终存储在堆中。处理器使用分配的字符串缓冲区的地址。指针,如 int,是处理器直接支持的主要数据类型之一。
  • 很好,是的,我理解那部分,但我想知道它是如何更深入地工作的。
  • 字符串是一个指针,不管它是如何工作的......
  • @old_timer 并不总是指针,例如在 C 中,一个小的 enougb 结构中的短 char[] 字符串可以在寄存器中传递,不需要指针。
  • @HansPassant 为什么 JIT 编译器会将在一个地方使用的短本地字符串放入堆中?也许现在所有的人都这样做,但我不明白他们为什么必须这样做。

标签: string function assembly


【解决方案1】:

在大多数高级语言中,数组(包括字符串)是通过引用传递的。 int foo(char*) 只是获取一个指针值作为 arg,而指针通常是一个机器字(即适合寄存器)。在良好的现代调用约定中,前几个整数/指针参数通常在寄存器中传递。

在 C/C++ 中,不能按值传递裸数组。给定int arr[16]; func(arr);,函数func 只获得一个指针(指向第一个元素)。

在其他一些更高级的语言中,数组可能更像 C++ std::vector,因此被调用者可能能够扩大/缩小数组并找出其长度而无需单独的 arg。这通常意味着有一个“控制块”。

在 C 和 C++ 中,您可以按值传递结构,然后由调用约定规则指定如何传递它们

例如,x86-64 System V 将 16 字节或更小的结构体传递到最多 2 个整数寄存器中。较大的结构被复制到堆栈上,无论它们包含多大的数组成员 (What kind of C11 data type is an array according to the AMD64 ABI)。 (所以不要将巨型对象按值传递给非内联函数!)

Windows x64 调用约定通过隐藏引用传递大型结构。

示例

typedef struct {
    // too big makes the asm output cluttered with loops or memcpy
    // int Big_McLargeHuge[1024*1024];
    int arr[4];
    long long a,b; //,c,d;
} bigobj;
// total 32 bytes with int=4, long long=8 bytes

int func(bigobj a);
int foo(bigobj a) {
    a.arr[3]++;
    return func(a);
}

source + asm output on the Godbolt compiler explorer.

您可以在 Godbolt 上尝试使用其标准调用约定的其他架构,例如 ARM 或 AArch64。我之所以选择 x86-64,是因为我碰巧知道在该平台上用于结构传递的两个主要调用约定之间存在一个有趣的差异。

x86-64 System V (gcc7.3 -O3)foo 有一个可以修改的 arg 的真实按值副本(由其调用者完成),因此它这样做并将其用作尾调用的 arg。 (如果不能tailcall,它就必须再做一个完整的副本。这个例子人为地让System V看起来很好)。

foo(bigobj):
    add     DWORD PTR [rsp+20], 1   # increment the struct member in the arg on the stack
    jmp     func(bigobj)            # tailcall func(a)

x86-64 Windows (MSVC CL19 /Ox):注意我们通过 RCX 寻址 a.arr[3],第一个整数/指针 arg。所以有一个隐藏的引用,但它不是一个常量引用。 这个函数是按值调用的,但它正在修改它通过引用获得的数据。所以调用者必须复制,或者至少假设被调用者破坏了它得到的指针。 (如果对象在那之后死了,则不需要复制,但这仅适用于本地结构对象,而不是传递指向全局的指针或其他东西)。

$T1 = 32    ; offset of the tmp copy in this function's stack frame
foo PROC
    sub      rsp, 72              ; 00000048H     ; 32B of shadow space + 32B bigobj + 8 to align
    inc      DWORD PTR [rcx+12]
    movups   xmm0, XMMWORD PTR [rcx]              ; load modified `a`
    movups   xmm1, XMMWORD PTR [rcx+16]           ; apparently alignment wasn't required
    lea      rcx, QWORD PTR $T1[rsp]
    movaps   XMMWORD PTR $T1[rsp], xmm0
    movaps   XMMWORD PTR $T1[rsp+16], xmm1         ; store a copy
    call     int __cdecl func(struct bigobj)
    add      rsp, 72              ; 00000048H
    ret      0
foo ENDP

制作对象的另一个副本似乎是错过了优化。我认为对于相同的调用约定,这将是foo 的有效实现:

foo:
    add      DWORD PTR [rcx+12], 1       ; more efficient than INC because of the memory dst, on Intel CPUs
    jmp      func                        ; tailcall with pointer still in RCX

SysV ABI 的 x86-64 clang 也错过了 gcc7.3 发现的优化,并且确实像 MSVC 一样复制

所以 ABI 的差异并没有我想象的那么有趣;在这两种情况下,被调用者都“拥有” arg,即使对于 Windows,它不能保证在堆栈上。我想这可以实现动态分配,以按值传递非常大的对象而不会出现堆栈溢出,但这有点毫无意义。只是一开始就不要这样做。


小物件:

x86-64 System V 传递打包到寄存器中的小对象。如果您注释掉 long long 成员,Clang 会找到一个巧妙的优化,这样您就可以拥有

typedef struct {
    int arr[4];
    //    long long a,b; //,c,d;
} bigobj;

# clang6.0 -O3
foo(bigobj):                          # @foo(bigobj)
    movabs  rax, 4294967296    # 0x100000000 = 1ULL << 32
    add     rsi, rax
    jmp     func(bigobj)          # TAILCALL

arr[0..1] 被打包到 RDI 中,arr[2..3] 被打包到 RSI 中,即 x86-64 SysV ABI 中的前 2 个整数/指针参数传递寄存器)。

gcc 将arr[3] 解压到一个寄存器中,它可以在其中递增它。

但是 clang 不是解包和重新打包,而是通过添加 1ULL&lt;&lt;32 来增加 RSI 的高 32 位。

MSVC 仍然通过隐藏引用传递,并且仍然复制整个对象。

【讨论】:

    【解决方案2】:

    为什么不试试呢?

       const char str[]="some string, doesnt matter how long";
        void more_fun ( const char *, const char *, int);
    
        void fun ( void )
        {
            more_fun(str,"hello world",5);
        }
    

    一个让链接器满意的虚拟函数

    .globl more_fun
    more_fun:
        bx lr
    

    架构与这个问题并不真正相关,编译器解决这个特定问题的方式与通用指令集相同,这些指令集具有一组基本的寻址模式等......所以如果有一个例外,我不是在说关于那些平台,但是x86、arm、mips、powerpc等等等等,都会属于这一类。

    链接和反汇编,你会看到已经知道的东西,因为根据定义,字符串变量是指向某事物开头的指针(只是一个地址,没什么更令人兴奋的):

    Disassembly of section .text:
    
    00001000 <fun>:
        1000:   e92d4010    push    {r4, lr}
        1004:   e3a02005    mov r2, #5
        1008:   e59f100c    ldr r1, [pc, #12]   ; 101c <fun+0x1c>
        100c:   e59f000c    ldr r0, [pc, #12]   ; 1020 <fun+0x20>
        1010:   eb000003    bl  1024 <more_fun>
        1014:   e8bd4010    pop {r4, lr}
        1018:   e12fff1e    bx  lr
        101c:   0000104c    andeq   r1, r0, r12, asr #32
        1020:   00001028    andeq   r1, r0, r8, lsr #32
    
    00001024 <more_fun>:
        1024:   e12fff1e    bx  lr
    
    Disassembly of section .rodata:
    
    00001028 <str>:
        1028:   656d6f73    strbvs  r6, [sp, #-3955]!   ; 0xfffff08d
        102c:   72747320    rsbsvc  r7, r4, #32, 6  ; 0x80000000
        1030:   2c676e69    stclcs  14, cr6, [r7], #-420    ; 0xfffffe5c
        1034:   656f6420    strbvs  r6, [pc, #-1056]!   ; c1c <fun-0x3e4>
        1038:   20746e73    rsbscs  r6, r4, r3, ror lr
        103c:   7474616d    ldrbtvc r6, [r4], #-365 ; 0xfffffe93
        1040:   68207265    stmdavs r0!, {r0, r2, r5, r6, r9, r12, sp, lr}
        1044:   6c20776f    stcvs   7, cr7, [r0], #-444 ; 0xfffffe44
        1048:   00676e6f    rsbeq   r6, r7, pc, ror #28
        104c:   6c6c6568    cfstr64vs   mvdx6, [r12], #-416 ; 0xfffffe60
        1050:   6f77206f    svcvs   0x0077206f
        1054:   00646c72    rsbeq   r6, r4, r2, ror r12
    

    因为这是来自 objdump 它只是试图反汇编字符串,就好像它是指令一样,所以忽略文本部分的反汇编。

    【讨论】:

    • 立即字符串的存储变为只读/文本。如果您有运行时初始化的作为局部变量的字符串,则字符串本身当然会在堆栈上(如果不是静态的),但指针仍然是指针,而字符串变量只是指针,因此可以使用传递给被调用者指针不是整个字符串。结构等都以这种方式工作。有时,如果语言定义了在调用之前会在堆栈上创建一个副本,但调用使用的是指针,一个地址。
    • 如果我太笼统了,我很抱歉。这里的实际要点是通过简单地尝试自己回答问题是多么微不足道......对于情况X,这些工具会产生什么。
    • 对于 x86、arm 等,还有编译器遵循的调用标准/定义,该文档将描述这些情况,以及如何传递什么的规则。
    猜你喜欢
    • 2010-12-07
    • 2018-04-19
    • 2021-07-10
    • 2011-03-06
    • 2011-01-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多