当您具有大输入的函数时,在汇编级别会发生什么

时间:2018-05-02 18:54:10

标签: string function assembly

虽然JavaScript没有直接成为汇编,但它应该演示一个普遍的问题,即如果函数的输入很大,如果在汇编中实现了高级函数,它将是什么样子。比如说这种情况:

myfunc(1, 2, 3)

变量有小整数,因此它们可以放在单独的寄存器上。但是说你有:

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

想知道如何在装配(高级别)中完成。

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

2 个答案:

答案 0 :(得分:1)

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

在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

您可以使用标准调用约定(如ARM或AArch64)尝试使用Godbolt上的其他体系结构。我选择了x86-64,因为我碰巧知道在这个平台上用于结构传递的两个主要调用约定的一个有趣的区别。

x86-64系统V(gcc7.3 -O3foo具有其arg的真实按值副本(由其调用者完成),它可以修改,所以它这样做并将其用作尾调用的arg。 (如果它不能进行尾调用,则必须制作另一个完整副本。这个例子人为地使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(第一个整数/指针arg)来解决a.arr [3]。所以有一个隐藏的引用,但它不是const引用。 此函数由值调用,但它正在修改通过引用获取的数据。所以调用者必须复制,或者至少假设一个被调用者破坏了它指向的arg。 (如果对象在此之后死亡,则不需要复制,但这仅适用于本地struct对象,而不是用于将指针传递给全局或其他东西)。

$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

x86-64 clang for SysV ABI也错过了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个整数/指针arg传递寄存器。

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

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

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

答案 1 :(得分:0)

为什么不试试呢?

   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,它只是试图反汇编字符串,就好像它是指令一样,因此对文本部分进行反汇编。