Pull to refresh

Comments 55

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

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

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


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


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


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

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

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


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


Как я понимаю, это поле просто имеет размер 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%, но довольно надежно.
UFO just landed and posted this here

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


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

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

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


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

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

Вы подобрали пример, в котором лишили компилятор возможности помочь вам с типами данных и работой с ними. Зачем? Возможно, передача структуры с двумя полями (строка и целое) решит проблему? Всякие стандарты вроде MISRA-C уже после void* валерьянку пьют, а после memcpy да и без проверки на успешность выделения памяти так и рыдать и курить начинают.
Не учите олимпиадников жить. Только олимпиадник может отстрелить себе ногу, держа верёвку этой же ногой.
Извините, но если это — образец олимпиадного кода, то я был об олимпиадниках лучшего мнения…
Подобное может написать либо олимпиадник, либо глубоко несчастный человек, вынужденный использовать очень специфичный API, написанный олимпиадником.
Совершенно очевидно, что это «одноразовый» код, т.к. его невозможно отлаживать и поддерживать. Так только олимпиадники и начинающие программисты накодить могут.
Ещё программист, которого разбудили заказчики посреди ночи, и которому срочно (ещё вчера) нужен костыль на продакшен, где каждая минута простоя стоит какие-нибудь мегабабки. Впрочем, это укладывается во вторую категорию, глубоко несчастных :)
Уточните, пожалуйста, какой именно у вас ARM?
А не пробовали echo 2 > /proc/cpu/alignment? Мне в свое время помогло
Для доступа к невыровненным данным можно написать функцию, по типу uint32_t get_unaligned_be32(void * ptr); в которой по байтам считать данные и собрать их в dword
Довольно тривиальная задача, не так ли? Проверяем на компе (x86), все ОК.

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


struct Bar
{
  char string[10];
  int  numeric;
};
//...
Bar bp;
memset(bp.string, 0, 10);
strcpy(bp.string, "123");
bp.numeric = 1234567;
foo(&bp);
Сейчас всё починю
#pragma pack(push, 1)
struct Bar
{
  char string[10];
  int  numeric;
};
#pragma pack(pop)
Вы ничего не починили. Нуль-терминант в 4 байте, а число начиная с 11 байта. Так что foo отработает не правильно. Выравниванием тут ничего не изменишь.
На самом деле починил: компилятор сгенерирует другой код для доступа к полю numeric, видя, что оно не выравнено.
Дело не в выравнивании. Foo ожидает, что строка ограничена нулём, затем идёт число. Сразу! А в примере записано три символа, нуль, а потом оставшиеся 6 байт массива. И только после них число. Тут весь смысл примера RPG18, что в функцию нельзя передавать вот такую простую структурку. Если же записать в неё 9 символов + 0, то тогда отработает правильно, если выравнивание ожидаемо сработает.
Обычно такие задачи решаются выравниванием изначальных данных, а не подставлением костылей для невыровненных. Это и для х86 справедливо — обращение по невыровненным данным хоть и не приведёт к подобной «ошибке», но ухудшит производительность (ну и на выравние в структурах многие на грабли наступают)
В иных архитектурах при доступе к невыровненным данным вообще аппаратное исключение выбрасывается.
не проще 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);  //выводим данные
}
Это немного другая задача. В исходной задаче мы не знаем какой размер у строки. Знаем только что она нультерминальная.
да без разницы

struct bar {
  char* str;
  int value;
}

struct bar mybar = { .str="blablabla", .value=0x12345678 };

...

foo(&mybar);

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

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


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

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

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

Обращение по не выровненному адресу — популярные грабли при программировании 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 побайтно из не выровненого адреса.

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

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

Sign up to leave a comment.

Articles