2016-08-08 34 views
28

Ich habe einen Byte-Puffer gefüllt mit Datensätzen variabler Länge, deren Länge durch das erste Byte des Datensatzes bestimmt wird. Eine reduzierte Version einer C Funktion einen einzelnen Datensatz zu lesenWarum erzeugt der Compiler 4-Byte-Ladevorgänge statt 1-Byte-Ladevorgänge, bei denen die breitere Last auf nicht zugeordnete Daten zugreifen kann?

void mach_parse_compressed(unsigned char* ptr, unsigned long int* val) 
{ 
    if (ptr[0] < 0xC0U) { 
     *val = ptr[0] + ptr[1]; 
     return; 
    } 

    *val = ((unsigned long int)(ptr[0]) << 24) 
     | ((unsigned long int)(ptr[1]) << 16) 
     | ((unsigned long int)(ptr[2]) << 8) 
     | ptr[3]; 
} 

erzeugt Baugruppe (GCC 5.4 -O2 -fPIC auf x86_64), die ersten vier Bytes bei PTR lädt, vergleicht das erste Byte mit 0xC0 und verarbeitet dann entweder zwei, entweder vier Bytes. Die undefinierten Bytes werden korrekt verworfen, aber warum denkt Compiler, dass es sicher ist, vier Byte an erster Stelle zu laden? Da es keine z.B. Ausrichtungsanforderung für ptr, es kann auf die letzten zwei Bytes einer Speicherseite zeigen, die neben einem nicht zugeordneten alle bekannten ist, was zu einem Absturz führt.

Sowohl -fPIC als auch -O2 oder höher müssen reproduziert werden.

Fehle ich hier etwas? Ist der Compiler korrekt und wie kann ich umgehen?

kann ich die oben zeigen Valgrind/AddressSanitiser Fehler erhalten oder einen Absturz mit mmap/mprotect:

//#define HEAP 
#define MMAP 
#ifdef MMAP 
#include <unistd.h> 
#include <sys/mman.h> 
#include <stdio.h> 
#elif HEAP 
#include <stdlib.h> 
#endif 

void 
mach_parse_compressed(unsigned char* ptr, unsigned long int* val) 
{ 
    if (ptr[0] < 0xC0U) { 
     *val = ptr[0] + ptr[1]; 
     return; 
    } 

    *val = ((unsigned long int)(ptr[0]) << 24) 
     | ((unsigned long int)(ptr[1]) << 16) 
     | ((unsigned long int)(ptr[2]) << 8) 
     | ptr[3]; 
} 

int main(void) 
{ 
    unsigned long int val; 
#ifdef MMAP 
    int error; 
    long page_size = sysconf(_SC_PAGESIZE); 
    unsigned char *buf = mmap(NULL, page_size * 2, PROT_READ | PROT_WRITE, 
           MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); 
    unsigned char *ptr = buf + page_size - 2; 
    if (buf == MAP_FAILED) 
    { 
     perror("mmap"); 
     return 1; 
    } 
    error = mprotect(buf + page_size, page_size, PROT_NONE); 
    if (error != 0) 
    { 
     perror("mprotect"); 
     return 2; 
    } 
    *ptr = 0xBF; 
    *(ptr + 1) = 0x10; 
    mach_parse_compressed(ptr, &val); 
#elif HEAP 
    unsigned char *buf = malloc(16384); 
    unsigned char *ptr = buf + 16382; 
    buf[16382] = 0xBF; 
    buf[16383] = 0x10; 
#else 
    unsigned char buf[2]; 
    unsigned char *ptr = buf; 
    buf[0] = 0xBF; 
    buf[1] = 0x10; 
#endif 
    mach_parse_compressed(ptr, &val); 
} 

MMAP Version:

Segmentation fault (core dumped) 

Mit Valgrind:

==3540== Process terminating with default action of signal 11 (SIGSEGV) 
==3540== Bad permissions for mapped region at address 0x4029000 
==3540== at 0x400740: mach_parse_compressed (in /home/laurynas/gcc-too-wide-load/gcc-too-wide-load) 
==3540== by 0x40060A: main (in /home/laurynas/gcc-too-wide-load/gcc-too-wide-load) 

Mit ASan:

ASAN:SIGSEGV 
================================================================= 
==3548==ERROR: AddressSanitizer: SEGV on unknown address 0x7f8f4dc25000 (pc 0x000000400d8a bp 0x0fff884e56c6 sp 0x7ffc4272b620 T0) 
    #0 0x400d89 in mach_parse_compressed (/home/laurynas/gcc-too-wide-load/gcc-too-wide-load+0x400d89) 
    #1 0x400b92 in main (/home/laurynas/gcc-too-wide-load/gcc-too-wide-load+0x400b92) 
    #2 0x7f8f4c72082f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f) 
    #3 0x400c58 in _start (/home/laurynas/gcc-too-wide-load/gcc-too-wide-load+0x400c58) 

AddressSanitizer can not provide additional info. 
SUMMARY: AddressSanitizer: SEGV ??:0 mach_parse_compressed 

HEAP Version mit Valgrind:

==30498== Invalid read of size 4 
==30498== at 0x400603: mach_parse_compressed (mach0data_reduced.c:9) 
==30498== by 0x4004DE: main (mach0data_reduced.c:34) 
==30498== Address 0x520703e is 16,382 bytes inside a block of size 16,384 alloc'd 
==30498== at 0x4C2DB8F: malloc (vg_replace_malloc.c:299) 
==30498== by 0x4004C0: main (mach0data_reduced.c:24) 

Stack-Version mit Asan:

==30528==ERROR: AddressSanitizer: stack-buffer-overflow on address 
0x7ffd50000440 at pc 0x000000400b63 bp 0x7ffd500003c0 sp 
0x7ffd500003b0 
READ of size 4 at 0x7ffd50000440 thread T0 
    #0 0x400b62 in mach_parse_compressed 
CMakeFiles/innobase.dir/mach/mach0data_reduced.c:15 
    #1 0x40087e in main CMakeFiles/innobase.dir/mach/mach0data_reduced.c:34 
    #2 0x7f3be2ce282f in __libc_start_main 
(/lib/x86_64-linux-gnu/libc.so.6+0x2082f) 
    #3 0x400948 in _start 
(/home/laurynas/obj-percona-5.5-release/storage/innobase/CMakeFiles/innobase.dir/mach/mach0data_test+0x400948) 

Dank

EDIT: hinzugefügt MMAP-Version, die tatsächlich abstürzt, geklärte Compiler-Optionen

EDIT 2: meldete es als https://gcc.gnu.org/bugzilla/show_bug.cgi?id=77673. Um das Problem zu umgehen, löst das Einfügen eines Compiler-Speicherbarrieren asm volatile("": : :"memory"); nach der if-Anweisung das Problem. Danke allen!

+5

Klingt wie ein Compiler-Fehler. Vielleicht möchten Sie einen Fehlerbericht einreichen. –

+4

Möglich, aber wollte hier zuerst mit Sprache Anwälte/Compiler-Experten überprüfen, zu oft ein offensichtlicher Compiler Bug ist Benutzerfehler –

+2

Der Compiler könnte wissen, dass diese 4-Byte-Last nie einen Absturz auf der Zielarchitektur (Valgrind-Bericht ungeachtet). Wenn Sie ein Beispiel erstellen könnten, das tatsächlich abstürzt, würde dies den Fall für einen Compiler-Fehler stärken. –

Antwort

2

Herzlichen Glückwunsch! Du hast einen echten Compilerfehler gefunden!

Sie können http://gcc.godbolt.org verwenden, um Baugruppenausgaben von verschiedenen Compilern und Optionen zu untersuchen.

Mit gcc Version 6.2 für x86 64-Bit-Linux, mit gcc -fPIC -O2, wird Ihre Funktion falsche Code kompilieren:

mach_parse_compressed(unsigned char*, unsigned long*): 
    movzbl (%rdi), %edx 
    movl (%rdi), %eax ; potentially incorrect load of 4 bytes 
    bswap %eax 
    cmpb $-65, %dl 
    jbe  .L5 
    movl %eax, %eax 
    movq %rax, (%rsi) 
    ret 
.L5: 
    movzbl 1(%rdi), %eax 
    addl %eax, %edx 
    movslq %edx, %rdx 
    movq %rdx, (%rsi) 
    ret 

Sie richtig das Problem und das mmap Beispiel liefert Test eine gute Regression diagnostiziert. gcc versucht zu stark, diese Funktion zu optimieren, und der resultierende Code ist definitiv falsch: das Lesen von 4 Bytes von einer nicht ausgerichteten Adresse ist für die meisten X86-Betriebsumgebungen in Ordnung, aber das Lesen über das Ende eines Arrays hinaus nicht. Der Compiler könnte davon ausgehen, dass Lesevorgänge nach dem Ende eines Arrays OK sind, wenn sie eine 32-Bit- oder sogar 64-Bit-Grenze nicht überschreiten, aber diese Annahme ist für Ihr Beispiel nicht korrekt. Sie können möglicherweise einen Absturz für einen Block mit malloc zugewiesen bekommen, wenn Sie es groß genug machen. malloc verwendet mmap für sehr große Blöcke (> = 128 KB standardmäßig IRCC).

Beachten Sie, dass Bug mit Version 5.1 des Compilers eingeführt wurde.

clang auf der anderen Seite hat dieses Problem nicht, aber der Code scheint weniger effizient im allgemeinen Fall:

# @mach_parse_compressed(unsigned char*, unsigned long*) 
mach_parse_compressed(unsigned char*, unsigned long*):   
    movzbl (%rdi), %ecx 
    cmpq $191, %rcx 
    movzbl 1(%rdi), %eax 
    ja  .LBB0_2 
    addq %rcx, %rax 
    movq %rax, (%rsi) 
    retq 
.LBB0_2: 
    shlq $24, %rcx 
    shlq $16, %rax 
    orq  %rcx, %rax 
    movzbl 2(%rdi), %ecx 
    shlq $8, %rcx 
    orq  %rax, %rcx 
    movzbl 3(%rdi), %eax 
    orq  %rcx, %rax 
    movq %rax, (%rsi) 
    retq 
1

Es scheint Compiler den Zugriff auf ptr zu optimieren. Es ist möglich, die Optimierung für den Zugriff auf ptr zu deaktivieren, indem Sie einfach das Schlüsselwort volatile hinzufügen. In diesem Fall gibt es keinen Absturz für die MMAP-Variante.

//#define HEAP 
#define MMAP 
#ifdef MMAP 
#include <unistd.h> 
#include <sys/mman.h> 
#include <stdio.h> 
#elif HEAP 
#include <stdlib.h> 
#endif 

void 
mach_parse_compressed(volatile unsigned char* ptr, unsigned long int* val) 
{ 
    if (ptr[0] < 0xC0U) { 
     *val = ptr[0] + ptr[1]; 
     return; 
    } 

    *val = ((unsigned long int)(ptr[0]) << 24) 
     | ((unsigned long int)(ptr[1]) << 16) 
     | ((unsigned long int)(ptr[2]) << 8) 
     | ptr[3]; 
} 

int main(void) 
{ 
    unsigned long int val; 
#ifdef MMAP 
    int error; 
    long page_size = sysconf(_SC_PAGESIZE); 
    unsigned char *buf = (unsigned char *) mmap(NULL, page_size * 2, PROT_READ | PROT_WRITE, 
           MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); 
    unsigned char *ptr = buf + page_size - 2; 
    if (buf == MAP_FAILED) 
    { 
     perror("mmap"); 
     return 1; 
    } 
    error = mprotect(buf + page_size, page_size, PROT_NONE); 
    if (error != 0) 
    { 
     perror("mprotect"); 
     return 2; 
    } 
    *ptr = 0xBF; 
    *(ptr + 1) = 0x10; 
    mach_parse_compressed(ptr, &val); 
#elif HEAP 
    unsigned char *buf = malloc(16384); 
    unsigned char *ptr = buf + 16382; 
    buf[16382] = 0xBF; 
    buf[16383] = 0x10; 
#else 
    unsigned char buf[2]; 
    unsigned char *ptr = buf; 
    buf[0] = 0xBF; 
    buf[1] = 0x10; 
#endif 
    mach_parse_compressed(ptr, &val); 
} 
+0

Ich frage mich (wird später versuchen), wenn ich eine Speicherbarriere anstelle von schweren flüchtigen-Hammer für eine Problemumgehung einfügen kann –

+0

Yep, ein "asm volatile ("::: "Speicher"); " nach der if-Anweisung ist eine Arbeit, und pessimize Code nicht. –

1

Bei einigen Architekturen (z.B. STM32), eine 4-Byte-Lade-/Speicheroperation wird auf dem Segment 4-Byte angewandt, in dem der Operand "angeordnet" ist.

Zum Beispiel wird ein 4-Byte-Ladevorgang von Adresse 0x80000003 auf 0x80000000 angewendet.

Darüber hinaus bildet der Speicherbus einen Adressraum ab, der bei einer ausgerichteten 4-Byte-Adresse beginnt und eine ganze Zahl von 4-Byte-Segmenten enthält.

Zum Beispiel beginnt der Adressraum bei 0 (inklusive) und endet bei 0x80000000 (exklusiv).

Nehmen wir nun an, dass wir eine solche Architektur verwenden und den Bus so konfigurieren, dass er den gesamten Adressraum lesen (laden) kann.

Anschließend wird eine 4-Byte-Ladeoperation erfolgreich (ohne einen Busfehler zu verursachen) irgendwo innerhalb des gegebenen Adressraums abgeschlossen.


auch sagen, dass dies auf x86/x64 soweit nicht der Fall ist, wie ich mir bewusst bin ...