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)
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 же классический.Bombus
03.09.2016 23:40Тег «ошибки» заиграл новыми красками.
DmitryKoterov
04.09.2016 00:50+1Да ошибка там просто в коде, должно быть:
memcpy(&value, str+strlen(str)+1, sizeof(value)); //копируем в нее данные
WinPooh73
03.09.2016 21:15+3Не говоря о том, что копировать данные для решения такой простой задачи — вообще, мягко говоря, не самый экономный подход…
Bombus
03.09.2016 23:16Если копировать только участок под исходным числом в участок занятый конечной переменной, то вполне. Имхо так и делается, если исходная переменная оказалась и с одной и с другой стороны границы выравнивания. Т.е. если структуру упаковать принудительно, то этот код будет меньше в объеме, но медленне, т.к. вначале будет выполнятся копирование, а затем уже пойдут вычисления.
Да чего рассказываю, когда хороша ссылка есть.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??? Как это может работать? Сама суть статьи была не оттестирована? Я в замешательстве.
LynXzp
04.09.2016 02:40Со строками в структуре нужно тоже быть аккуратным. В поле char str[5] строка «12345» войдет без нареканий и ворнингов, но терминального нуля не будет. А безразмерные строки как последний элемент структуры — вообще беда, нужно все время помнить что нельзя брать sizeof и ложить в массив такую структуру.
thewizardplusplus
04.09.2016 17:48Почему нельзя брать sizeof от структуры с безразмерными строками и почему нельзя класть их в массив?
sizeof не проходит символы строки, ему без разницы, есть там нулевой символ или нет. Размер структуры определяется на стадии компиляции и sizeof разрешается на стадии компиляции. Размер определяется на основании размеров типов полей в структуре (и учёта выравнивания, конечно же), а уж тип у любого поля точно есть, и его размер компилятору точно известен.
Аналогично с массивом — раз размер структуры известен, то ничто не помешает положить её в массив и оперировать с ней в последствии.
Единственное, что нельзя вызывать на таких строках — это функции для работы со строками (в том числе передавать их в printf). Потому что именно они полагаются на наличие терминального символа.
LynXzp
04.09.2016 18:42Проверил, оказывается в С++ это даже не скомилируется, но работает в С89 и С99. На всякий случай: я имел в виду что последним элементом структуры может быть строка без указанных границ «char str[];» и ее размер вообще не будет учитываться в sizeof. Соответственно с массивом то же самое. Как можно брать элемент массива, если мы не знаем границ даже первого элемента (вручную нав. можно найти терминальный нуль после каждого элемента, но я не уверен что это безопасно). В качестве доказательства код: https://2.bp.blogspot.com/.../snapshot.png
thewizardplusplus
04.09.2016 19:12Извините, неверно вас понял. Я не знал, что возможно определение массива без размера как поля структуры. Спасибо за объяснение.
Однако такой код компилируется во всех стандартах, как в C89 и C99, так и в C++14 (проверял через флаг -std). Правда C++ не даёт инициализировать такое поле, говоря, что любая строка слишком длинная для него. Однако можно инициализировать значением по умолчанию. И C++ позволяет брать от такой структуры sizeof (с тем же результатом, как и в C) и при печати читает за границей памяти структуры.
Как я понимаю, это поле просто имеет размер 0. И соответственно, его смещение указывает на конец структуры с поправкой на выравнивание. Может, это можно даже использовать как-то? ) Например, для детекта, была ли включена упаковка для стуктуры.
Но вы правы, не для строк явно.
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%, но довольно надежно.kloppspb
04.09.2016 23:49> с помощью структур можно детектить переполнения буферов
В большинстве случаев для это хватает и статических анализаторов кода. Не хватает — valgrind в помощь. В прочем, «метод DEADBEEF» тоже никто не отменял, да :)
dreamer-dead
05.09.2016 21:38То, что код компилируется, ничего не значит.
Массивы нулевой длины разрешены в C99, но не в С++(любого стандарта).
И в GCC и в Clang это реализовано через расширение языка, например -Wzero-length-array у Clang.
См. код http://coliru.stacked-crooked.com/a/810283a668408e8a
Использовать это понятно как, например принять по сети пакет с данными переменной длины, но с фиксированным заголовком и указанной длиной буфера.
thewizardplusplus
06.09.2016 01:38То, что код компилируется, ничего не значит.
Я это понимаю, но понадеялся, что ключ
-std
гарантирует использование только стандартизованных возможностей. Однако с ним код компилируется безо всяких предупреждений.
Но вы правы, вместе с ключом
-pedantic
пишет таки, что это нестандартная возможность. И это хорошо.
Anvol
03.09.2016 22:22+2Допустим у нас есть функция, которая принимает в себя указатель. Мы знаем, что в указателе лежит нуль-терминальная строка, а за ней 4-байтное целое.
Вы подобрали пример, в котором лишили компилятор возможности помочь вам с типами данных и работой с ними. Зачем? Возможно, передача структуры с двумя полями (строка и целое) решит проблему? Всякие стандарты вроде MISRA-C уже после void* валерьянку пьют, а после memcpy да и без проверки на успешность выделения памяти так и рыдать и курить начинают.maaGames
04.09.2016 07:46+3Не учите олимпиадников жить. Только олимпиадник может отстрелить себе ногу, держа верёвку этой же ногой.
WinPooh73
04.09.2016 12:14+2Извините, но если это — образец олимпиадного кода, то я был об олимпиадниках лучшего мнения…
maaGames
04.09.2016 12:21Подобное может написать либо олимпиадник, либо глубоко несчастный человек, вынужденный использовать очень специфичный API, написанный олимпиадником.
Совершенно очевидно, что это «одноразовый» код, т.к. его невозможно отлаживать и поддерживать. Так только олимпиадники и начинающие программисты накодить могут.WinPooh73
04.09.2016 12:30Ещё программист, которого разбудили заказчики посреди ночи, и которому срочно (ещё вчера) нужен костыль на продакшен, где каждая минута простоя стоит какие-нибудь мегабабки. Впрочем, это укладывается во вторую категорию, глубоко несчастных :)
xorbot
04.09.2016 00:00+1Для доступа к невыровненным данным можно написать функцию, по типу uint32_t get_unaligned_be32(void * ptr); в которой по байтам считать данные и собрать их в dword
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);
AndreyDmitriev
04.09.2016 10:07+1Сейчас всё починю
#pragma pack(push, 1) struct Bar { char string[10]; int numeric; }; #pragma pack(pop)
maaGames
04.09.2016 12:38Вы ничего не починили. Нуль-терминант в 4 байте, а число начиная с 11 байта. Так что foo отработает не правильно. Выравниванием тут ничего не изменишь.
jcmvbkbc
04.09.2016 21:54На самом деле починил: компилятор сгенерирует другой код для доступа к полю numeric, видя, что оно не выравнено.
maaGames
05.09.2016 14:05Дело не в выравнивании. Foo ожидает, что строка ограничена нулём, затем идёт число. Сразу! А в примере записано три символа, нуль, а потом оставшиеся 6 байт массива. И только после них число. Тут весь смысл примера RPG18, что в функцию нельзя передавать вот такую простую структурку. Если же записать в неё 9 символов + 0, то тогда отработает правильно, если выравнивание ожидаемо сработает.
AndreyDmitriev
04.09.2016 10:06Обычно такие задачи решаются выравниванием изначальных данных, а не подставлением костылей для невыровненных. Это и для х86 справедливо — обращение по невыровненным данным хоть и не приведёт к подобной «ошибке», но ухудшит производительность (ну и на выравние в структурах многие на грабли наступают)
WinPooh73
04.09.2016 12:38В иных архитектурах при доступе к невыровненным данным вообще аппаратное исключение выбрасывается.
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); //выводим данные }
CasualLinux
04.09.2016 15:03Это немного другая задача. В исходной задаче мы не знаем какой размер у строки. Знаем только что она нультерминальная.
spot62
04.09.2016 17:16да без разницы
struct bar { char* str; int value; } struct bar mybar = { .str="blablabla", .value=0x12345678 }; ... foo(&mybar);
CasualLinux
06.09.2016 18:30Это тоже немножко не то. В таком случае рядом с value будет лежать в памяти указатель на строку, а не она сама строка, как в исходном примере.
dipsy
04.09.2016 15:05+1А это не проблема ли компилятора GCC? И как это соотносится с требованиями стандарта С++?
Тоже волею судеб вынужден в последнее время натыкаться на подобные грабли, портируя код на ARM. Крайне неприятное поведение, в самых неожиданных местах может быть засада. С другой стороны вынуждает поменьше использовать сишное приведение типов и побольше покрывать всё тестами, поэтому пока не понял как к этому относиться, ругаться или хвалить.
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; }
artyom_belov
04.09.2016 15:05В статье вполне живой пример. Например парсинг бинарных данных, а не «дикая» передача аргументов. На входе строка байтов с одним выравнивание, на выходе — типизированые данные, у которых может быть своё выравнивание. Чтобы не сломать все приходится использовать приведение типов через memcpy.
CasualLinux
04.09.2016 15:08Собственно из реальной задачи паркинга бинарных данных, где инты и строки лежат в перемешку такая проблема и вылезла.
spot62
04.09.2016 17:34как уже писали выше https://habrahabr.ru/post/309144/#comment_9787398 обработка строк типа «blablabla\0\0\0» будет ломать код, поэтому strlen для вычисления смещения неприменим
Ivan_83
05.09.2016 02:56+1Memory 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
Ни гцц ни с++ тут вообще ни причём. Речь про аппаратные особенности доступа к памяти.
Слышал что на итаниумах всё ещё веселее.dipsy
05.09.2016 11:42А разве компилятор и не предназначен в том числе и для того, чтобы абстрагироваться от аппаратных особенностей? Мы же не думаем о страницах в памяти, о физических адресах, например. Или же это стандартом не оговаривается? Я вот искренне не понимаю, почему int x= *(int*)&foo.bar не работает, а memcopy от того же в точности адреса &foo.bar внезапно работает. Ладно, выравнивание, ладно можно понять что sizeof структуры, где int и char, равен 8, но почему адрес то смещается при приведении его к указателю на другой тип?
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() простая и быстрая.
mmMike
05.09.2016 06:08int value = (int)(str+strlen(str)+1);
На некоторых процах такое выражение вызовет segmentation fault с большой вероятность.
memcpy(&value, str+strlen(str)+1, sizeof(int)); //копируем в нее данные
совершенно не корректно. Поскольку это потенциальные грабли предполагающие что порядок байт размер int отправителя и получателя данных одинаков.
int бывает разный...
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 побайтно из не выровненого адреса.
Error1024
05.09.2016 16:02Не очень понятно на кого ориентирована данная статья, если для начинающих — то нет нормального описания Aligned, если для профессионалов — сомневаюсь что они оценят данный код и подход(совсем не оценят и будут правы).
Dark_Purple
05.09.2016 22:07Загружаем на борду с ARM
int16_t выравнен на границу 2 байт, int32_t выравнен на границу 4 байт, это знает даже школьник.
WinPooh73
Как-то оно небезопасно выглядит. Что будете делать, если размер пришедшей строки будет, скажем, мегабайт триста? То есть заведомо больше стек-фрейма?
CasualLinux
Пример максимально упрощен. Естественно, нужно наложить несколько проверок чтобы превратить его в реальный рабочий код.
WinPooh73
Ещё раз. В реальном рабочем коде вы будете копировать на стек строку размером 300 мегабайт? Если да, то какие проверки спасут вас от stack overflow? Если нет, то каковы дальнейшие действия вашего алгоритма?
CasualLinux
В примере в стек копируется 4 байта. В реальном коде никто копировать 300 МБ не будет конечно же.
WinPooh73
Исправление увидел, спасибо. Данный вопрос снят.