2016-08-08 3 views
28

У меня есть байтовый буфер, заполненный записями переменной длины, длина которых определяется первым байтом записи. Уменьшенная версия функции C, чтобы прочитать одну записьПочему компилятор генерирует 4-байтовую нагрузку вместо 1-байтовой нагрузки, где более широкая нагрузка может иметь доступ к неснятым данным?

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

генерирует сборки (НКУ 5.4 -O2 -fPIC на x86_64), который загружает четыре байта на PTR сначала сравнивает первый байт с 0xC0, а затем обрабатывает либо два, либо четыре байта. Неопределенные байты выбрасываются правильно, но почему компилятор считает, что безопасно загружать четыре байта в первую очередь? Поскольку нет, например, требование выравнивания для ptr, оно может указывать на последние два байта страницы памяти, которая находится рядом с неотображенной для всех, что мы знаем, что приводит к сбою.

Для воспроизведения требуется как -fpIC, так и -O2 или выше.

Я что-то упустил? Правильно ли это компилятор, и как мне это решить?

я могу получить выше показать ошибки Valgrind/AddressSanitiser или аварии с 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 версии:

Segmentation fault (core dumped) 

С 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) 

С 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 

КУЧА версия с 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 с Асан:

==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) 

Благодарности

EDIT: добавил MMAP версию, что на самом деле происходит сбой, осветленные опции компилятора

EDIT 2: сообщил об этом как https://gcc.gnu.org/bugzilla/show_bug.cgi?id=77673. Для обходного пути вставка защитного барьера компилятора asm volatile("": : :"memory"); после заявления if устраняет проблему. Всем спасибо!

+5

Похоже, ошибка компилятора. Возможно, вы захотите записать отчет об ошибке. –

+4

Возможно, но сначала нужно проверить с экспертами по языку/экспертами компилятора, слишком часто кажущаяся ошибка компилятора - ошибка пользователя –

+2

Компилятор может иметь представление о том, что эта 4-байтная загрузка никогда не может привести к сбою в целевой архитектуре (несмотря на отчет valgrind). Если бы вы могли привести пример, который действительно сработает, это укрепит ситуацию для ошибки компилятора. –

ответ

2

Поздравляем! Вы нашли подлинную ошибку компилятора!

Вы можете использовать http://gcc.godbolt.org для изучения сборочных результатов различных компиляторов и опций.

С GCC версии 6.2 для x86 64-битной Linux, используя gcc -fPIC -O2, ваша функция не компилировать некорректной код:

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 

Вы правильно диагностировали проблему и mmap пример дает хороший тест регрессии. gcc пытается слишком сильно оптимизировать эту функцию, и полученный код определенно неверен: чтение 4 байтов с неравномерного адреса в порядке для большинства операционных сред X86, но чтение за концом массива - нет.

Компилятор может предположить, что считывание за конец массива ОК, если они не пересекают границу 32-разрядной или даже 64-разрядной границы, но это предположение неверно для вашего примера. Возможно, вы сможете получить сбой для блока, выделенного malloc, если вы сделаете его достаточно большим. malloc использует mmap для очень больших блоков (> = 128 Кбайт по умолчанию IRCC).

Обратите внимание, что этот ошибка была представлена ​​с версией 5.1 компилятора.

clang с другой стороны, не имеют этой проблемы, но код кажется менее эффективным в общем случае:

# @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

Кажется, компилятор оптимизирует доступ к ptr. Можно отключить оптимизацию для доступа к ptr, просто добавив ключевое слово volatile. В этом случае для MMAP-варианта нет сбоя.

//#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

Мне интересно (попробуем позже), если я могу вставить барьер памяти вместо тяжелого летучего молотка для обходного пути –

+0

Yep, «asm volatile (« »:::« memory »); после того, как оператор if работает, и не пессимизирует код. –

1

На некоторых архитектурах (например, STM32), 4-байтовые операции загрузки/магазина применяются на сегменте 4 байта, в котором операнд «размещаются».

Например, 4-байтовая загрузка с адреса 0x80000003 будет применена на 0x80000000.

В дополнение к этому, шина памяти отображает адресное пространство, которое начинается с 4-байтового выровненного адреса и содержит целое число из 4-байтовых сегментов.

Например, адресное пространство начинается от 0 (включительно) и заканчивается на 0x80000000 (исключая).

Теперь предположим, что мы берем такую ​​архитектуру и настраиваем шину, чтобы разрешить чтение (загрузку) по всему адресному пространству.

Впоследствии операция загрузки по 4 байта будет выполнена успешно (без возникновения сбоя шины) в любом месте внутри данного адресного пространства.


Сказав, что это не так, на x86/x64, насколько я знаю ...