Подтвердить что ты не робот

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

У меня есть байтовый буфер, заполненный записями переменной длины, длина которых определяется первым байтом записи. Сокращенная версия функции C для чтения одной записи

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

генерирует сборку (GCC 5.4 -O2 -fPIC на x86_64), которая сначала загружает четыре байта в ptr, сравнивает первый байт с 0xC0 и затем обрабатывает либо два, либо четыре байта. Байты undefined выбрасываются правильно, но почему компилятор считает безопасным загрузить четыре байта в первую очередь? Поскольку нет, например, требование выравнивания для 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

Версия HEAP с 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)

Версия стека с 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)

Спасибо

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

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

4b9b3361

Ответ 1

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

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

С gcc версии 6.2 для 64-разрядного Linux-сервера x86 с помощью 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

Ответ 2

Кажется, компилятор оптимизирует доступ к 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);
}

Ответ 3

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

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

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

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

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

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


Сказав это, это не так на x86/x64, насколько мне известно...