Строгое правило, антиалиасинг и реализации функция strlen библиотеки glibc


Я уже некоторое время читаю о строгом правиле сглаживания и начинаю по-настоящему путаться. Прежде всего, я прочитал эти вопросы и некоторые ответы:

Согласно им (насколько я понимаю), доступ к буферу char с помощью указателя на другой тип нарушает строгое правило сглаживания. Однако реализация glibc strlen() имеет такой код (с комментариями и удаленной 64-битной реализацией):

size_t strlen(const char *str)
{
    const char *char_ptr;
    const unsigned long int *longword_ptr;
    unsigned long int longword, magic_bits, himagic, lomagic;

    for (char_ptr = str; ((unsigned long int) char_ptr 
             & (sizeof (longword) - 1)) != 0; ++char_ptr)
       if (*char_ptr == '')
           return char_ptr - str;

    longword_ptr = (unsigned long int *) char_ptr;

    himagic = 0x80808080L;
    lomagic = 0x01010101L;

    for (;;)
    { 
        longword = *longword_ptr++;

        if (((longword - lomagic) & himagic) != 0)
        {
            const char *cp = (const char *) (longword_ptr - 1);

            if (cp[0] == 0)
                return cp - str;
            if (cp[1] == 0)
                return cp - str + 1;
            if (cp[2] == 0)
                return cp - str + 2;
            if (cp[3] == 0)
                return cp - str + 3;
        }
    }
}

Строка longword_ptr = (unsigned long int *) char_ptr;, очевидно, переименовывает unsigned long int в char. Я не могу понять, что делает это возможным. Я вижу, что код заботится о проблемах выравнивания, поэтому никаких проблем нет, но я думаю, что это не связано со строгим правилом псевдонимирования.

Принятый ответ на третий связанный вопрос гласит:

Однако существует очень распространенное расширение компилятора, позволяющее приводить правильно выровненные указатели из char в другие типы и обращаться к ним, однако это нестандартно.

Единственное, что приходит мне на ум, это вариант -fno-strict-aliasing, так ли это? Я нигде не смог найти документально подтвержденного того, от чего зависят разработчики glibc, и комментарии каким-то образом подразумевают, что это приведение выполняется без каких-либо проблем, поскольку очевидно, что проблем не будет. Это заставляет меня думать, что это действительно так очевидно, и я упускаю что-то глупое, но мои поиски не увенчались успехом.

4 6

4 ответа:

В ISO C этот код нарушает строгое правило псевдонимирования. (А также нарушить правило, что вы не можете определить функцию с тем же именем, что и стандартная библиотечная функция). Однако этот код не подчиняется правилам ISO C. стандартная библиотека даже не должна быть реализована на языке C-like. Стандарт только указывает, что реализация реализует поведение стандартных функций.

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

При написании правил псевдонимирования авторы стандарта рассматривали только те формы, которые были бы полезны и, следовательно, должны быть обязательными для всех реализаций. Реализации на языке с нацелены на различные цели, и авторы стандарта не делают попыток указать, что компилятор должен делать, чтобы быть пригодным для какой-либо конкретной цели (например, низкоуровневое программирование) или, если уж на то пошло, для какой-либо цели вообще.

Код, подобный приведенному выше, который опирается на низкоуровневый не следует ожидать, что конструкции будут выполняться на компиляторах, которые не претендуют на пригодность для низкоуровневого программирования. С другой стороны, любой компилятор, который не может поддерживать такой код, должен рассматриваться как неподходящий для низкоуровневого программирования. Обратите внимание, что компиляторы могут использовать предположения об алиасировании на основе типов и по-прежнему подходят для низкоуровневого программирования , если прилагают разумные усилия для распознавания общих шаблонов алиасирования. Некоторые авторы компиляторов очень сильно вкладываются в представление кода который подходит для не стандартных низкоуровневых моделей кодирования, ни c стандарт, но любой, кто пишет низкоуровневый код, должен просто признать, что эти компиляторы' оптимизаторы не подходят для работы с низкоуровневым кодом.

Формулировка стандарта на самом деле немного более странная, чем фактические реализации компилятора: стандарт C говорит об объявленных типах объектов, но компиляторы всегда видят только указатели на эти объекты. Таким образом, когда компилятор видит приведение от char* к unsigned long*, он должен предположить, что char* на самом деле алиасирует объект с объявленным типом unsigned long, делая приведение правильным.

Слово предостережения: я предполагаю, что strlen() компилируется в библиотеку, которая будет только позже связан с остальной частью приложения. Таким образом, оптимизатор не видит использования функции при ее компиляции, вынуждая его предполагать, что приведение к unsigned long* действительно является законным. Если вы позвонили strlen() с

short myString[] = {0x666f, 0x6f00, 0};
size_t length = strlen((char*)myString);    //implementation now invokes undefined behavior!

Приведение в strlen() является неопределенным поведением, и вашему компилятору было бы разрешено удалить почти все тело strlen(), если бы он увидел, что вы используете его при компиляции strlen(). Единственное, что позволяет strlen() вести себя так, как ожидается в этом вызове, - это то, что strlen() является компилируется отдельно как библиотека, скрывая неопределенное поведение от оптимизатора, поэтому оптимизатор должен считать приведение допустимым при компиляции strlen().

Таким образом, если предположить, что оптимизатор не может вызвать "неопределенное поведение", причина, по которой приведения от char* к чему-либо другому опасны, заключается не в сглаживании, а в выравнивании. На некоторых аппаратных средствах странные вещи начинают происходить, если вы пытаетесь получить доступ к смещенному указателю. Оборудование может загрузить данные с неправильного адреса, вызвать прерывание, или просто обрабатывайте запрошенную нагрузку на память крайне медленно. Вот почему стандарт C, как правило, заявляет, что такие забросы к неопределенному поведению. Тем не менее, вы видите, что рассматриваемый код фактически обрабатывает проблему выравнивания явно (первый цикл, который содержит подусловие (unsigned long int) char_ptr & (sizeof (longword) - 1)). После этого char* Правильно выравнивается, чтобы быть переинтерпретированным как unsigned long*.

Конечно, все это на самом деле не соответствует стандарту C, но это соответствует стандарту C реализация компилятора, с помощью которого этот код должен быть скомпилирован. Если люди gcc модифицировали свой компилятор, чтобы действовать на этот бит кода, люди glibc просто жаловались бы на это достаточно громко, чтобы gcc был изменен обратно, чтобы правильно обрабатывать этот вид приведения.

В конце концов, стандартные реализации библиотеки C просто должны нарушать строгие правила псевдонимирования, чтобы работать должным образом и быть эффективными. strlen() просто нужно нарушать эти правила, чтобы быть эффективным, malloc()/free() функция pair должна иметь возможность взять область памяти, которая имела объявленный тип Foo, и превратить ее в область памяти объявленного типа Bar. И нет никакого вызова malloc() внутри реализации malloc(), который дал бы объекту объявленный тип в первую очередь. Абстракция языка Си просто разрушается на этом уровне.

Основное предположение, вероятно, заключается в том, что функция компилируется отдельно и недоступна для встроенных или других перекрестных оптимизаций функций. Это означает, что информация о времени компиляции не течет ни внутри, ни вне функции.

Функция не пытается изменить что-либо с помощью указателя, поэтому конфликта нет.