Допустим у нас есть функция, которая принимает в себя указатель. Мы знаем, что в указателе лежит нуль-терминальная строка, а за ней 4-байтное целое. Задача — вывести в консоль строку и целое. Решить можно вот так:

void foo(void* data_ptr)
{
  //Ставим указатель на строку на начало данных
  char* str = (char*)data_ptr;
  //А указатель на целое смещаем на длину строки и еще один байт
  int* value = (int*)(str+strlen(str)+1);
  //и выводим содержимое указателей
  printf("%s %d", str, *value);
}

Довольно тривиальная задача, не так ли? Проверяем на компе (x86), все ОК. Загружаем на борду с ARM. И, не успев выстрелить себе в ногу, наступаем на грабли. В зависимости от содержания строки, целое значение выводится то нормальным, то кривым. Поверяем указатели, проверяем память, на которые они указывают. Все в норме.

Подмечаем, что целое выводится ровно, когда длина строки равна 3, 7, 11, ..., 4*n-1. Ага. По внимательней смотрим на память и на вывод в «кривых» случаях. Например, если память выглядит так:

Адрес:

|0x00|0x01|0x02|0x03|0x04|0x05|0x06|0x07|0x08|

Данные:

|0x31|0x31|0x31|0x31|0x00|0x01|0x00|0x00|0x00|

На выходе мы получаем строку «1111» и целое 0x00000100 вместо 0x00000001.

Вывод: Несмотря на то, что выражением *value мы обращаемся по указателю 0x05, данные нам возвращаются как-будто обращение происходит по указателю 0x04 (или другому кратному 4).

Так как правильно решить такую задачу? А вот так:


void foo(void* data_ptr)
{
  int value; //Выделяем переменную на стеке
  char* str = (char*)data_ptr; 
  memcpy(&value, str+strlen(str)+1, sizeof(int)); //копируем в нее данные
  printf("%s %d", str, value);  //выводим данные
}


В таком случае все всегда на своих местах.
Спасибо за внимание!

UPD: Исправил очевидную ошибку.
Поделиться с друзьями
-->

Комментарии (55)


  1. WinPooh73
    03.09.2016 21:05

    Как-то оно небезопасно выглядит. Что будете делать, если размер пришедшей строки будет, скажем, мегабайт триста? То есть заведомо больше стек-фрейма?


    1. CasualLinux
      04.09.2016 12:07

      Пример максимально упрощен. Естественно, нужно наложить несколько проверок чтобы превратить его в реальный рабочий код.


      1. WinPooh73
        04.09.2016 12:11
        +1

        Ещё раз. В реальном рабочем коде вы будете копировать на стек строку размером 300 мегабайт? Если да, то какие проверки спасут вас от stack overflow? Если нет, то каковы дальнейшие действия вашего алгоритма?


        1. CasualLinux
          04.09.2016 12:21

          В примере в стек копируется 4 байта. В реальном коде никто копировать 300 МБ не будет конечно же.


          1. WinPooh73
            04.09.2016 12:25

            Исправление увидел, спасибо. Данный вопрос снят.


  1. WinPooh73
    03.09.2016 21:10
    +4

    Да и вообще, вызов вами функции memcpy не соответствует её сигнатуре. В третьем параметре должен быть размер копируемой области, а не указатель на её конец.
    memcpy(&value, str, str+strlen(str)+1)); //копируем в нее данные

    https://ru.wikipedia.org/wiki/Memcpy

    void *memcpy(void *dst, const void *src, size_t n);

    где
    dst — адрес буфера назначения
    srс — адрес источника
    n — количество байт для копирования

    Так что то, что у вас «все всегда на своих местах» — это результат какого-то невероятного везения :))
    Undefined behaviour же классический.


    1. Bombus
      03.09.2016 23:40

      Тег «ошибки» заиграл новыми красками.


      1. DmitryKoterov
        04.09.2016 00:50
        +1

        Да ошибка там просто в коде, должно быть:

        memcpy(&value, str+strlen(str)+1, sizeof(value)); //копируем в нее данные


        1. maaGames
          04.09.2016 07:43
          +2

          «Просто в 50% кода ошибка».


    1. CasualLinux
      04.09.2016 12:22

      Верно. Исправил.


  1. WinPooh73
    03.09.2016 21:15
    +3

    Не говоря о том, что копировать данные для решения такой простой задачи — вообще, мягко говоря, не самый экономный подход…


    1. Bombus
      03.09.2016 23:16

      Если копировать только участок под исходным числом в участок занятый конечной переменной, то вполне. Имхо так и делается, если исходная переменная оказалась и с одной и с другой стороны границы выравнивания. Т.е. если структуру упаковать принудительно, то этот код будет меньше в объеме, но медленне, т.к. вначале будет выполнятся копирование, а затем уже пойдут вычисления.
      Да чего рассказываю, когда хороша ссылка есть.


      1. Bombus
        03.09.2016 23:37
        +1

        Забыл добавить, не понравилось, что исходя из сигнатуры memcpy:
        void *memcpy(void *dst, const void *src, size_t n);
        была использована перегруженная функция:
        memcpy(&value, str, str+strlen(str)+1));
        Зачем? Да и этом случае копируется вся строка с довеском. В переменную value??? Как это может работать? Сама суть статьи была не оттестирована? Я в замешательстве.


      1. LynXzp
        04.09.2016 02:40

        Со строками в структуре нужно тоже быть аккуратным. В поле char str[5] строка «12345» войдет без нареканий и ворнингов, но терминального нуля не будет. А безразмерные строки как последний элемент структуры — вообще беда, нужно все время помнить что нельзя брать sizeof и ложить в массив такую структуру.


        1. thewizardplusplus
          04.09.2016 17:48

          Почему нельзя брать sizeof от структуры с безразмерными строками и почему нельзя класть их в массив?


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


          Аналогично с массивом — раз размер структуры известен, то ничто не помешает положить её в массив и оперировать с ней в последствии.


          Единственное, что нельзя вызывать на таких строках — это функции для работы со строками (в том числе передавать их в printf). Потому что именно они полагаются на наличие терминального символа.


          1. LynXzp
            04.09.2016 18:42

            Проверил, оказывается в С++ это даже не скомилируется, но работает в С89 и С99. На всякий случай: я имел в виду что последним элементом структуры может быть строка без указанных границ «char str[];» и ее размер вообще не будет учитываться в sizeof. Соответственно с массивом то же самое. Как можно брать элемент массива, если мы не знаем границ даже первого элемента (вручную нав. можно найти терминальный нуль после каждого элемента, но я не уверен что это безопасно). В качестве доказательства код: https://2.bp.blogspot.com/.../snapshot.png


            1. thewizardplusplus
              04.09.2016 19:12

              Извините, неверно вас понял. Я не знал, что возможно определение массива без размера как поля структуры. Спасибо за объяснение.


              Однако такой код компилируется во всех стандартах, как в C89 и C99, так и в C++14 (проверял через флаг -std). Правда C++ не даёт инициализировать такое поле, говоря, что любая строка слишком длинная для него. Однако можно инициализировать значением по умолчанию. И C++ позволяет брать от такой структуры sizeof (с тем же результатом, как и в C) и при печати читает за границей памяти структуры.


              Как я понимаю, это поле просто имеет размер 0. И соответственно, его смещение указывает на конец структуры с поправкой на выравнивание. Может, это можно даже использовать как-то? ) Например, для детекта, была ли включена упаковка для стуктуры.


              Но вы правы, не для строк явно.


              1. LynXzp
                04.09.2016 19:37

                Да, я просто написал покороче, в двух предложениях два разных случая, что могло ввести в заблуждение. (У меня g++ -std=c++11 -Wno-error tmp.c -o tmp.o и обругал как Вы говорите и не скомпилировал, а std=c++14 не понял :) gcc 4.8.4. Удивительно если в новом добавили.)

                Для детекта упаковки можно сделать так:
                { struct {uint8_t c1;uint8_t c2;}tmp_st;
                typedef char tmp[sizeof(tmp_st)==2? 1:-1 ]; }
                Если размер не совпадет с 2 — не скомпилируется и без assert. И не надо что-то выдумывать, просто брать sizeof нужного элемента и делать такой typedef.

                Вспомнилось что с помощью структур можно детектить переполнения буферов: буфер помещается в структуру, после буфера магическое число. Если магическое число изменилось — было переполнение. Не 100%, но довольно надежно.


                1. kloppspb
                  04.09.2016 23:49

                  > с помощью структур можно детектить переполнения буферов

                  В большинстве случаев для это хватает и статических анализаторов кода. Не хватает — valgrind в помощь. В прочем, «метод DEADBEEF» тоже никто не отменял, да :)


              1. dreamer-dead
                05.09.2016 21:38

                То, что код компилируется, ничего не значит.
                Массивы нулевой длины разрешены в C99, но не в С++(любого стандарта).
                И в GCC и в Clang это реализовано через расширение языка, например -Wzero-length-array у Clang.
                См. код http://coliru.stacked-crooked.com/a/810283a668408e8a


                Использовать это понятно как, например принять по сети пакет с данными переменной длины, но с фиксированным заголовком и указанной длиной буфера.


                1. thewizardplusplus
                  06.09.2016 01:38

                  То, что код компилируется, ничего не значит.

                  Я это понимаю, но понадеялся, что ключ -std гарантирует использование только стандартизованных возможностей. Однако с ним код компилируется безо всяких предупреждений.


                  Но вы правы, вместе с ключом -pedantic пишет таки, что это нестандартная возможность. И это хорошо.


  1. Anvol
    03.09.2016 22:22
    +2

    Допустим у нас есть функция, которая принимает в себя указатель. Мы знаем, что в указателе лежит нуль-терминальная строка, а за ней 4-байтное целое.

    Вы подобрали пример, в котором лишили компилятор возможности помочь вам с типами данных и работой с ними. Зачем? Возможно, передача структуры с двумя полями (строка и целое) решит проблему? Всякие стандарты вроде MISRA-C уже после void* валерьянку пьют, а после memcpy да и без проверки на успешность выделения памяти так и рыдать и курить начинают.


    1. maaGames
      04.09.2016 07:46
      +3

      Не учите олимпиадников жить. Только олимпиадник может отстрелить себе ногу, держа верёвку этой же ногой.


      1. WinPooh73
        04.09.2016 12:14
        +2

        Извините, но если это — образец олимпиадного кода, то я был об олимпиадниках лучшего мнения…


        1. maaGames
          04.09.2016 12:21

          Подобное может написать либо олимпиадник, либо глубоко несчастный человек, вынужденный использовать очень специфичный API, написанный олимпиадником.
          Совершенно очевидно, что это «одноразовый» код, т.к. его невозможно отлаживать и поддерживать. Так только олимпиадники и начинающие программисты накодить могут.


          1. WinPooh73
            04.09.2016 12:30

            Ещё программист, которого разбудили заказчики посреди ночи, и которому срочно (ещё вчера) нужен костыль на продакшен, где каждая минута простоя стоит какие-нибудь мегабабки. Впрочем, это укладывается во вторую категорию, глубоко несчастных :)


  1. Amomum
    03.09.2016 23:23
    +1

    Уточните, пожалуйста, какой именно у вас ARM?


  1. Hacker13ua
    03.09.2016 23:47

    А не пробовали echo 2 > /proc/cpu/alignment? Мне в свое время помогло


  1. xorbot
    04.09.2016 00:00
    +1

    Для доступа к невыровненным данным можно написать функцию, по типу uint32_t get_unaligned_be32(void * ptr); в которой по байтам считать данные и собрать их в dword


  1. RPG18
    04.09.2016 00:05

    Довольно тривиальная задача, не так ли? Проверяем на компе (x86), все ОК.

    Сейчас всё сломаю:


    struct Bar
    {
      char string[10];
      int  numeric;
    };
    //...
    Bar bp;
    memset(bp.string, 0, 10);
    strcpy(bp.string, "123");
    bp.numeric = 1234567;
    foo(&bp);


    1. AndreyDmitriev
      04.09.2016 10:07
      +1

      Сейчас всё починю

      #pragma pack(push, 1)
      struct Bar
      {
        char string[10];
        int  numeric;
      };
      #pragma pack(pop)
      


      1. maaGames
        04.09.2016 12:38

        Вы ничего не починили. Нуль-терминант в 4 байте, а число начиная с 11 байта. Так что foo отработает не правильно. Выравниванием тут ничего не изменишь.


        1. jcmvbkbc
          04.09.2016 21:54

          На самом деле починил: компилятор сгенерирует другой код для доступа к полю numeric, видя, что оно не выравнено.


          1. maaGames
            05.09.2016 14:05

            Дело не в выравнивании. Foo ожидает, что строка ограничена нулём, затем идёт число. Сразу! А в примере записано три символа, нуль, а потом оставшиеся 6 байт массива. И только после них число. Тут весь смысл примера RPG18, что в функцию нельзя передавать вот такую простую структурку. Если же записать в неё 9 символов + 0, то тогда отработает правильно, если выравнивание ожидаемо сработает.


  1. AndreyDmitriev
    04.09.2016 10:06

    Обычно такие задачи решаются выравниванием изначальных данных, а не подставлением костылей для невыровненных. Это и для х86 справедливо — обращение по невыровненным данным хоть и не приведёт к подобной «ошибке», но ухудшит производительность (ну и на выравние в структурах многие на грабли наступают)


    1. WinPooh73
      04.09.2016 12:38

      В иных архитектурах при доступе к невыровненным данным вообще аппаратное исключение выбрасывается.


  1. spot62
    04.09.2016 13:00

    не проще void* приводить к указателю на исходную структуру, а в исходной структуре использовать требуемое для архитектуры выравнивание?

    struct bar {
      char str[256];
      int value;
    }
    
    ..
    
    void foo(void* data_ptr)
    {
      struct bar* pbar=(struct bar*) data_ptr; // приведение указателя
    
      printf("%s %d", pbar->str, pbar->value);  //выводим данные
    }
    


    1. CasualLinux
      04.09.2016 15:03

      Это немного другая задача. В исходной задаче мы не знаем какой размер у строки. Знаем только что она нультерминальная.


      1. spot62
        04.09.2016 17:16

        да без разницы

        struct bar {
          char* str;
          int value;
        }
        
        struct bar mybar = { .str="blablabla", .value=0x12345678 };
        
        ...
        
        foo(&mybar);
        
        


        1. CasualLinux
          06.09.2016 18:30

          Это тоже немножко не то. В таком случае рядом с value будет лежать в памяти указатель на строку, а не она сама строка, как в исходном примере.


  1. Calvrack
    04.09.2016 13:26
    +1

    Там а в итоге — это нарушает memory model в C или нет? Кто тут сломался?


  1. dipsy
    04.09.2016 15:05
    +1

    А это не проблема ли компилятора GCC? И как это соотносится с требованиями стандарта С++?
    Тоже волею судеб вынужден в последнее время натыкаться на подобные грабли, портируя код на ARM. Крайне неприятное поведение, в самых неожиданных местах может быть засада. С другой стороны вынуждает поменьше использовать сишное приведение типов и побольше покрывать всё тестами, поэтому пока не понял как к этому относиться, ругаться или хвалить.


  1. mynick
    04.09.2016 15:05

    Проверил на arm64 (смартфон с linuxdeploy), строка и целое выводятся правильно.

    int main(int argc, char *argv[]) {
            char buf[64];
            int magic=123456789;
            strcpy(buf, "hello");
            memcpy(buf+strlen(buf)+1, (void *)&magic, sizeof(magic));
    
            printf("%s %d\n", buf, *(int*)(buf+strlen(buf)+1));
    
            return 0;
    }
    


    1. CasualLinux
      06.09.2016 18:31

      На OMAP L132 все как в исходном примере.


  1. artyom_belov
    04.09.2016 15:05

    В статье вполне живой пример. Например парсинг бинарных данных, а не «дикая» передача аргументов. На входе строка байтов с одним выравнивание, на выходе — типизированые данные, у которых может быть своё выравнивание. Чтобы не сломать все приходится использовать приведение типов через memcpy.


    1. CasualLinux
      04.09.2016 15:08

      Собственно из реальной задачи паркинга бинарных данных, где инты и строки лежат в перемешку такая проблема и вылезла.


      1. spot62
        04.09.2016 17:34

        как уже писали выше https://habrahabr.ru/post/309144/#comment_9787398 обработка строк типа «blablabla\0\0\0» будет ломать код, поэтому strlen для вычисления смещения неприменим


  1. Ivan_83
    05.09.2016 02:56
    +1

    Memory aligment как он есть.
    Притом компилятор честно скажет на арме что это херня какая может получится.
    У меня на арме просто падало в таких случаях, кажется bus error сигнал был.
    На х86 обычно производительность просаживается если в цикле работа с не выровненными данными идёт, поэтому всякие mem*() хитро извращаются чтобы память была всегда выравнена при обращении.

    Пример конечно так себе с точки зрения кода, но вполне наглядный и удобный для игр на воспроизведение.
    У меня было быстрое сравнение с помощью не выровненного приведения в uint32_t или uint64_t. Кое где было копирование так же сделано. Пришлось всё на mem*() поменять, а для случаев когда всё таки uint8_t* приводится к чему то uint32_t* подписывать в коменте что это безопасно потому что… адрес точно выровнен/выше уже проверили выравнивание.

    Как выше заметили можно без копирования сделать так:
    static inline uint32_t
    U8TO32_LITTLE(const uint8_t *p) {
    return
    (((uint32_t)(p[0]) ) |
    ((uint32_t)(p[1]) << 8) |
    ((uint32_t)(p[2]) << 16) |
    ((uint32_t)(p[3]) << 24));
    }

    2 WinPooh73
    2 Anvol
    2 RPG18
    2 spot62
    Ну какие нафиг структуры, приведения и тп. — вы спорите с голосами в голове.
    Речь то не о том как получше на вход подавать, а том как сожрать то что уже дали.
    Жрать надо что дают, не везде программерам рестораны с заказными типами на входе.
    Как в случаях сжатия, хэширования, шифрования, парсинга…
    Автор по быстрому накидал код чтобы показать как воспроизвести то что ему порвало шаблоны :)

    2 Calvrack
    Никто не сломался, ничего не нарушено в С.
    Компилятор честно предупредит что возможно обращение по не выровненному адресу, а дальше сам решай правильно оно в твоём случае или нет.

    2 dipsy
    Ни гцц ни с++ тут вообще ни причём. Речь про аппаратные особенности доступа к памяти.
    Слышал что на итаниумах всё ещё веселее.


    1. dipsy
      05.09.2016 11:42

      А разве компилятор и не предназначен в том числе и для того, чтобы абстрагироваться от аппаратных особенностей? Мы же не думаем о страницах в памяти, о физических адресах, например. Или же это стандартом не оговаривается? Я вот искренне не понимаю, почему int x= *(int*)&foo.bar не работает, а memcopy от того же в точности адреса &foo.bar внезапно работает. Ладно, выравнивание, ладно можно понять что sizeof структуры, где int и char, равен 8, но почему адрес то смещается при приведении его к указателю на другой тип?


      1. Ivan_83
        08.09.2016 00:16
        +1

        Вы не думаете, думают другие :)
        Памятью управляет ОС, как и потоками и прочим.

        Потому что есть команда ассемблера, типа mov eax,[ptr] — чтобы поместить в регистр eax содержимое по адресу ptr.
        Компилятор не знает заранее значения ptr в ряде случаев и не может выдать больше варнинга.
        Проц же получая адрес у которого 0 != (3 & ptr) те не кратного 4 (или 2, 8...) и при этом нужно прочитать 4 байта (2, 8...) может либо сгенерировать исключение либо сам либо просто отбросить/проигнорировать младшие биты.
        У автора он игнорирует, у меня бросал исключение.

        Когда важна производительность то стараются выравнивать память.
        Когда выровнять не возможно то запросто могут быть несколько веток кода: для выровненной и не выровненной памяти.

        Не memcopy а memcpy.
        Есть ещё memmove() она корректно отрабатывает пересекающиеся регионы, те
        memcpy(ptr, (ptr + 1), ...) — скорее всего отработает не правильно, особенно если ptr однобайтовый.
        memmove() для пересекающихся регионов памяти, в ней больше проверок и логики, чтобы не запороть данные.
        memcpy() простая и быстрая.


  1. mmMike
    05.09.2016 06:08

    int value = (int)(str+strlen(str)+1);

    На некоторых процах такое выражение вызовет segmentation fault с большой вероятность.


    memcpy(&value, str+strlen(str)+1, sizeof(int)); //копируем в нее данные

    совершенно не корректно. Поскольку это потенциальные грабли предполагающие что порядок байт размер int отправителя и получателя данных одинаков.
    int бывает разный...


  1. MooNDeaR
    05.09.2016 14:39

    Собственно в статье есть все, кроме инфы о выравнивании в ARM.


  1. olegator99
    05.09.2016 14:41
    +2

    Обращение по не выровненному адресу — популярные грабли при программировании embed. Обычно это вызывает исключение, однако в вашем случае исключения процессора unaligned access были отключены/не предусмотрены процессором. (Кстати правда, а что у вас за чип)?


    Таких приведений указателей лучше избегать, однако если очень хочется, то в gcc >= 4.8 есть специальный ключ ''-mno-unaligned-access" — он автоматически генерит код обращения к полям типов с учетом выравнивания:


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


    typedef int unaligned_int __attribute ((__aligned__(1)));
    
    void  foo(unaligned_int *addr)
    {
       printf ("%d",*addr);  
    }
    

    Скомпилируем просто
    gcc -Wall -O3


    foo(int*):
            ldr     r2, [r0]  @ unaligned
            movw    r1, #:lower16:.LC0
            movs    r0, #1
            movt    r1, #:upper16:.LC0
            b       __printf_chk
    .LC0:
            .ascii  "%d\000"
    

    А теперь с ключем -mno-unaligned-access
    gcc -Wall -O3 -mno-unaligned-access:


    foo(int*):
            push    {r4, r5, r6}
            mov     r4, r0
            ldrb    r6, [r0, #1]    @ zero_extendqisi2
            movw    r1, #:lower16:.LC0
            ldrb    r3, [r0]        @ zero_extendqisi2
            movt    r1, #:upper16:.LC0
            ldrb    r5, [r4, #2]    @ zero_extendqisi2
            movs    r0, #1
            ldrb    r2, [r4, #3]    @ zero_extendqisi2
            orr     r3, r3, r6, lsl #8
            orr     r3, r3, r5, lsl #16
            orr     r2, r3, r2, lsl #24
            pop     {r4, r5, r6}
            b       __printf_chk
    .LC0:
            .ascii  "%d\000"
    

    Обратите внимание — компилятор сам нагенерил кода, который вытягивает и собирает int побайтно из не выровненого адреса.


  1. Error1024
    05.09.2016 16:02

    Не очень понятно на кого ориентирована данная статья, если для начинающих — то нет нормального описания Aligned, если для профессионалов — сомневаюсь что они оценят данный код и подход(совсем не оценят и будут правы).


  1. Dark_Purple
    05.09.2016 22:07

    Загружаем на борду с ARM

    int16_t выравнен на границу 2 байт, int32_t выравнен на границу 4 байт, это знает даже школьник.