О выравнивании памяти на ARM процессорах на простом примере

Допустим у нас есть функция, которая принимает в себя указатель. Мы знаем, что в указателе лежит нуль-терминальная строка, а за ней 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: Исправил очевидную ошибку.

Similar posts

Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 55

    0
    Как-то оно небезопасно выглядит. Что будете делать, если размер пришедшей строки будет, скажем, мегабайт триста? То есть заведомо больше стек-фрейма?
      0
      Пример максимально упрощен. Естественно, нужно наложить несколько проверок чтобы превратить его в реальный рабочий код.
        +1
        Ещё раз. В реальном рабочем коде вы будете копировать на стек строку размером 300 мегабайт? Если да, то какие проверки спасут вас от stack overflow? Если нет, то каковы дальнейшие действия вашего алгоритма?
          0
          В примере в стек копируется 4 байта. В реальном коде никто копировать 300 МБ не будет конечно же.
            0
            Исправление увидел, спасибо. Данный вопрос снят.
      +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 же классический.
        0
        Тег «ошибки» заиграл новыми красками.
          +1
          Да ошибка там просто в коде, должно быть:

          memcpy(&value, str+strlen(str)+1, sizeof(value)); //копируем в нее данные
            +2
            «Просто в 50% кода ошибка».
          0
          Верно. Исправил.
          +3
          Не говоря о том, что копировать данные для решения такой простой задачи — вообще, мягко говоря, не самый экономный подход…
            0
            Если копировать только участок под исходным числом в участок занятый конечной переменной, то вполне. Имхо так и делается, если исходная переменная оказалась и с одной и с другой стороны границы выравнивания. Т.е. если структуру упаковать принудительно, то этот код будет меньше в объеме, но медленне, т.к. вначале будет выполнятся копирование, а затем уже пойдут вычисления.
            Да чего рассказываю, когда хороша ссылка есть.
              +1
              Забыл добавить, не понравилось, что исходя из сигнатуры memcpy:
              void *memcpy(void *dst, const void *src, size_t n);
              была использована перегруженная функция:
              memcpy(&value, str, str+strlen(str)+1));
              Зачем? Да и этом случае копируется вся строка с довеском. В переменную value??? Как это может работать? Сама суть статьи была не оттестирована? Я в замешательстве.
                0
                Со строками в структуре нужно тоже быть аккуратным. В поле char str[5] строка «12345» войдет без нареканий и ворнингов, но терминального нуля не будет. А безразмерные строки как последний элемент структуры — вообще беда, нужно все время помнить что нельзя брать sizeof и ложить в массив такую структуру.
                  0

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


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


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


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

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

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


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


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


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

                        0
                        Да, я просто написал покороче, в двух предложениях два разных случая, что могло ввести в заблуждение. (У меня 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%, но довольно надежно.
                          0
                          > с помощью структур можно детектить переполнения буферов

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

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


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

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

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


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

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

                Вы подобрали пример, в котором лишили компилятор возможности помочь вам с типами данных и работой с ними. Зачем? Возможно, передача структуры с двумя полями (строка и целое) решит проблему? Всякие стандарты вроде MISRA-C уже после void* валерьянку пьют, а после memcpy да и без проверки на успешность выделения памяти так и рыдать и курить начинают.
                  +3
                  Не учите олимпиадников жить. Только олимпиадник может отстрелить себе ногу, держа верёвку этой же ногой.
                    +2
                    Извините, но если это — образец олимпиадного кода, то я был об олимпиадниках лучшего мнения…
                      0
                      Подобное может написать либо олимпиадник, либо глубоко несчастный человек, вынужденный использовать очень специфичный API, написанный олимпиадником.
                      Совершенно очевидно, что это «одноразовый» код, т.к. его невозможно отлаживать и поддерживать. Так только олимпиадники и начинающие программисты накодить могут.
                        0
                        Ещё программист, которого разбудили заказчики посреди ночи, и которому срочно (ещё вчера) нужен костыль на продакшен, где каждая минута простоя стоит какие-нибудь мегабабки. Впрочем, это укладывается во вторую категорию, глубоко несчастных :)
                  +1
                  Уточните, пожалуйста, какой именно у вас ARM?
                    0
                    А не пробовали echo 2 > /proc/cpu/alignment? Мне в свое время помогло
                      +1
                      Для доступа к невыровненным данным можно написать функцию, по типу uint32_t get_unaligned_be32(void * ptr); в которой по байтам считать данные и собрать их в dword
                        0
                        Довольно тривиальная задача, не так ли? Проверяем на компе (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
                          Сейчас всё починю
                          #pragma pack(push, 1)
                          struct Bar
                          {
                            char string[10];
                            int  numeric;
                          };
                          #pragma pack(pop)
                          
                            0
                            Вы ничего не починили. Нуль-терминант в 4 байте, а число начиная с 11 байта. Так что foo отработает не правильно. Выравниванием тут ничего не изменишь.
                              0
                              На самом деле починил: компилятор сгенерирует другой код для доступа к полю numeric, видя, что оно не выравнено.
                                0
                                Дело не в выравнивании. Foo ожидает, что строка ограничена нулём, затем идёт число. Сразу! А в примере записано три символа, нуль, а потом оставшиеся 6 байт массива. И только после них число. Тут весь смысл примера RPG18, что в функцию нельзя передавать вот такую простую структурку. Если же записать в неё 9 символов + 0, то тогда отработает правильно, если выравнивание ожидаемо сработает.
                          0
                          Обычно такие задачи решаются выравниванием изначальных данных, а не подставлением костылей для невыровненных. Это и для х86 справедливо — обращение по невыровненным данным хоть и не приведёт к подобной «ошибке», но ухудшит производительность (ну и на выравние в структурах многие на грабли наступают)
                            0
                            В иных архитектурах при доступе к невыровненным данным вообще аппаратное исключение выбрасывается.
                            0
                            не проще 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);  //выводим данные
                            }
                            
                              0
                              Это немного другая задача. В исходной задаче мы не знаем какой размер у строки. Знаем только что она нультерминальная.
                                0
                                да без разницы

                                struct bar {
                                  char* str;
                                  int value;
                                }
                                
                                struct bar mybar = { .str="blablabla", .value=0x12345678 };
                                
                                ...
                                
                                foo(&mybar);
                                
                                
                                  0
                                  Это тоже немножко не то. В таком случае рядом с value будет лежать в памяти указатель на строку, а не она сама строка, как в исходном примере.
                              +1
                              Там а в итоге — это нарушает memory model в C или нет? Кто тут сломался?
                                +1
                                А это не проблема ли компилятора GCC? И как это соотносится с требованиями стандарта С++?
                                Тоже волею судеб вынужден в последнее время натыкаться на подобные грабли, портируя код на ARM. Крайне неприятное поведение, в самых неожиданных местах может быть засада. С другой стороны вынуждает поменьше использовать сишное приведение типов и побольше покрывать всё тестами, поэтому пока не понял как к этому относиться, ругаться или хвалить.
                                  0
                                  Проверил на 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;
                                  }
                                  
                                    0
                                    На OMAP L132 все как в исходном примере.
                                    0
                                    В статье вполне живой пример. Например парсинг бинарных данных, а не «дикая» передача аргументов. На входе строка байтов с одним выравнивание, на выходе — типизированые данные, у которых может быть своё выравнивание. Чтобы не сломать все приходится использовать приведение типов через memcpy.
                                      0
                                      Собственно из реальной задачи паркинга бинарных данных, где инты и строки лежат в перемешку такая проблема и вылезла.
                                        0
                                        как уже писали выше https://habrahabr.ru/post/309144/#comment_9787398 обработка строк типа «blablabla\0\0\0» будет ломать код, поэтому strlen для вычисления смещения неприменим
                                    • UFO just landed and posted this here
                                        0
                                        А разве компилятор и не предназначен в том числе и для того, чтобы абстрагироваться от аппаратных особенностей? Мы же не думаем о страницах в памяти, о физических адресах, например. Или же это стандартом не оговаривается? Я вот искренне не понимаю, почему int x= *(int*)&foo.bar не работает, а memcopy от того же в точности адреса &foo.bar внезапно работает. Ладно, выравнивание, ладно можно понять что sizeof структуры, где int и char, равен 8, но почему адрес то смещается при приведении его к указателю на другой тип?
                                        • UFO just landed and posted this here
                                        0
                                        int value = (int)(str+strlen(str)+1);

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


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

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

                                          0
                                          Собственно в статье есть все, кроме инфы о выравнивании в ARM.
                                            +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 побайтно из не выровненого адреса.

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

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

                                                Only users with full accounts can post comments. Log in, please.