Manuelle Montage vs. GCC

Lesezeit: 5 Minuten

Benutzeravatar von trilean
trilean

Haftungsausschluss: Ich fange gerade erst mit der x86-Assemblierung an. Ich habe ein bisschen SPIM an der Universität gelernt, aber das ist kaum der Rede wert.

Ich dachte, ich beginne mit der wahrscheinlich einfachsten Funktion in libc, abs(). Ziemlich einfach in C:

long myAbs(long j) {
    return j < 0 ? -j : j;
}

Meine Version in Assembler:

    .global myAbs
    .type   myAbs, @function
    .text

myAbs:
    test %rdi, %rdi
    jns end
    negq %rdi
end:
    movq %rdi, %rax
    ret

(Dies funktioniert nicht für 32-Bit-Integer, wahrscheinlich weil RAX ein 64-Bit-Register ist und das Vorzeichen wahrscheinlich an der falschen Position steht – das muss ich untersuchen).

Hier ist nun, was gcc tut (gcc -O2 -S myAbs.c):

        .file   "myAbs.c"
        .section        .text.unlikely,"ax",@progbits
.LCOLDB0:
        .text
.LHOTB0:
        .p2align 4,,15
        .globl  myAbs
        .type   myAbs, @function
myAbs:
.LFB0:
        .cfi_startproc
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        subq    $4144, %rsp
        orq     $0, (%rsp)
        addq    $4128, %rsp
        movq    %rdi, %rdx
        sarq    $63, %rdx
        movq    %fs:40, %rax
        movq    %rax, -8(%rbp)
        xorl    %eax, %eax
        movq    %rdi, %rax
        xorq    %rdx, %rax
        subq    %rdx, %rax
        movq    -8(%rbp), %rcx
        xorq    %fs:40, %rcx
        jne     .L5
        leave
        .cfi_remember_state
        .cfi_def_cfa 7, 8
        ret
.L5:
        .cfi_restore_state
        call    __stack_chk_fail@PLT
        .cfi_endproc
.LFE0:
        .size   myAbs, .-myAbs
        .section        .text.unlikely
.LCOLDE0:
        .text
.LHOTE0:
        .ident  "GCC: (Gentoo Hardened 5.1.0 p1.2, pie-0.6.3) 5.1.0"
        .section        .note.GNU-stack,"",@progbits

Warum dieser große Unterschied? GCC produziert wesentlich mehr Anweisungen. Ich kann mir nicht vorstellen, dass dies nicht langsamer sein wird als mein Code. Übersehe ich etwas? Oder mache ich hier etwas gravierend falsch?

  • Vielleicht fühlt sich Ihr GCC unwohl? Meine produziert weniger Anweisungen. (Und Clang mag bedingte Züge.)

    – Kerrek SB

    21. Juli 2015 um 13:01 Uhr


  • “GCC: (Gentoo Hardened 5.1.0 p1.2, pie-0.6.3) 5.1.0” – Ich denke, das ist der Hinweis. Der gehärtete C-Compiler enthält Stack-Smashing-Schutz oder ähnliches.

    – davmac

    21. Juli 2015 um 13:09 Uhr


  • Viele der ersten Aufrufe bestehen darin, den Stack einzurichten und die Rücksendeadresse zu speichern (etwas, das Sie nicht tun). Scheint, als gäbe es einen Stack-Schutz. Vielleicht könnten Sie Ihre Compilereinstellungen optimieren, um etwas Overhead loszuwerden.

    – Carloabelli

    21. Juli 2015 um 13:09 Uhr


  • @ cabellicar123: Schade, es scheint, als sollte der Compiler herausfinden, dass dies eine Blattfunktion ist und keinen Stapelschutz benötigt.

    – Kerrek SB

    21. Juli 2015 um 13:13 Uhr

  • Ich stimme dafür, dies zu schließen, weil die seltsame Compilerausgabe von Ihnen stammt, wenn Sie versehentlich den falschen Compiler verwenden.

    – fuz

    21. Juli 2015 um 13:29 Uhr

Für diejenigen, die sich fragen, woher der generierte Code stammt, beachten Sie zunächst, dass GCC beim Kompilieren myAbs mit Stapelschutz wandeln Sie es in diese Form um

long myAbs(long j) {
    uintptr_t canary = __stack_chk_guard;

    register long result = j < 0 ? -j : j;

    if ( (canary = canary ^ __stack_chk_guard) != 0 )
        __stack_chk_fail();
}

Der einfach auszuführende Code j < 0 ? -j : j; ist

movq    %rdi, %rdx     ;RDX = j
movq    %rdi, %rax     ;RAX = j
sarq    $63, %rdx      ;RDX = 0 if j >=0, 0fff...ffh if j < 0
xorq    %rdx, %rax     ;Note: x xor 0ff...ffh = Not X, x xor 0 = x
                       ;RAX = j if j >=0, ~j if j < 0
subq    %rdx, %rax     ;Note: 0fff...ffh = -1
                       ;RAX = j+0 = j if j >= 0, ~j+1 = -j if j < 0
                       ;~j+1 = -j in two complement

Analysieren des generierten Codes, den wir erhalten

    pushq   %rbp
    movq    %rsp, %rbp       ;Standard prologue

    subq    $4144, %rsp      ;Allocate slight more than 4 KiB     
    orq     $0, (%rsp)       ;Perform a useless RW operation to test if there is enough stack space for __stack_chk_fail

    addq    $4128, %rsp      ;This leave 16 byte allocated for local vars

    movq    %rdi, %rdx       ;See above
    sarq    $63, %rdx        ;See above

    movq    %fs:40, %rax     ;Get the canary
    movq    %rax, -8(%rbp)   ;Save it as a local var
    xorl    %eax, %eax       ;Clear it

    movq    %rdi, %rax       ;See above
    xorq    %rdx, %rax       ;See above
    subq    %rdx, %rax       ;See above

    movq    -8(%rbp), %rcx   ;RCX = Canary
    xorq    %fs:40, %rcx     ;Check if equal to the original value
    jne     .L5              ;If not fail

    leave
    ret
.L5:
    call    __stack_chk_fail@PLT  ;__stack_chk_fail is noreturn

Alle zusätzlichen Anweisungen dienen also der Implementierung von Stack-Smashing-Protector.

Danke an FUZxxl für den Hinweis auf die Verwendung der ersten Anweisungen nach dem Prolog.

  • Der Code subtrahiert 4144 und addiert dann 4128 dazu %rsp um sicherzustellen, dass so viel zusätzlicher Stack-Platz zur Verfügung steht. Wenn dies nicht der Fall ist, können Sie die Stack-Prüfung umgehen, indem Sie zuerst den Stack erschöpfen (was dazu führt, dass der Prozess aus einem anderen Grund abstürzt, der kein fehlgeschlagener Stack-Check ist).

    – fuz

    21. Juli 2015 um 13:31 Uhr

  • @FUZxxl Könntest du das näher erläutern? Meinen Sie, es sollte vor einem Stapelüberlauf schützen? Mich wundert diese Anleitung

    Benutzer781847

    21. Juli 2015 um 13:36 Uhr


  • Stellen Sie sich den Fall vor, in dem der Stack fast voll ist und jemand den Canary überschreibt. In diesem Fall erkennt der Code möglicherweise, dass die Stapelprüfung fehlgeschlagen ist, aber nicht genügend Speicherplatz zum Ausführen vorhanden ist __stack_chk_fail, anstatt eine fette Warnung auszugeben, dass die Stack-Prüfung fehlgeschlagen ist, stürzt der Code einfach mit einem Segmentierungsfehler ab und verbirgt die Tatsache, dass jemand versucht hat, in das Programm einzudringen. Die Subtraktions-, Schreib- und Additionssequenz stellt sicher, dass etwa 4 KiB Stack-Speicherplatz verbleiben, was ausreichen sollte __stack_chk_fail laufen.

    – fuz

    21. Juli 2015 um 13:49 Uhr

Benutzeravatar von carloabelli
carloabelli

Viele der ersten Aufrufe bestehen darin, den Stapel einzurichten und die Rücksendeadresse zu speichern (etwas, das Sie nicht tun). Scheint, als gäbe es welche Stapelschutz los. Vielleicht könnten Sie Ihre Compilereinstellungen optimieren, um etwas Overhead loszuwerden.

Vielleicht fügen Sie Ihrem Compiler Flags hinzu, wie zum Beispiel: -fno-stack-protector könnte diesen Unterschied minimieren.

Ja, das ist wahrscheinlich langsamer als Ihre handschriftliche Zusammenstellung, bietet aber viel mehr Schutz und ist wahrscheinlich den geringen Mehraufwand wert.

Warum der Stapelschutz immer noch existiert, obwohl es sich um eine Blattfunktion handelt siehe hier.

1390970cookie-checkManuelle Montage vs. GCC

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy