Просматривая старый фрагмент кода, я наткнулся на какой-то ужас кодирования, подобный этому:
struct Foo
{
unsigned int bar;
unsigned char qux;
unsigned char xyz;
unsigned int etc;
};
void horror(const char* s1, const char* s2, const char* s3, const char* s4, Foo* foo)
{
sscanf(s1, "%u", &(foo->bar));
sscanf(s2, "%u", (unsigned int*) &(foo->qux));
sscanf(s3, "%u", (unsigned int*) &(foo->xyz));
sscanf(s4, "%u", &(foo->etc));
}
Итак, что на самом деле происходит во втором и третьем sscanf, когда переданный аргумент является преобразованием unsigned char* в unsigned int*, но со спецификатором формата для целого числа без знака? Что бы ни происходило, это происходит из-за неопределенного поведения, но почему это вообще «работает»?
Насколько я знаю, в этом случае приведение фактически ничего не делает (фактический тип аргументов, передаваемых как ..., неизвестен вызываемой функции). Однако это было в производстве в течение многих лет, и оно никогда не зависало, и окружающие значения, по-видимому, не перезаписываются, я полагаю, потому что все элементы структуры выровнены по 32 битам. Это даже чтение правильного значения на целевой машине (32-битный ARM с небольшим порядком байтов), но я думаю, что это больше не будет работать с другим порядком байтов.
Бонусный вопрос: каков самый чистый правильный способ сделать это? Я знаю, что теперь у нас есть спецификатор формата %hhu (очевидно, введенный в C++11), но как насчет устаревшего компилятора C89?
Обратите внимание, что в исходном вопросе было uint32_t вместо unsigned int и unsigned char вместо uint8_t, но это просто вводило в заблуждение и не по теме, и, кстати, исходный код, который я просматривал, использует свои собственные определения типов.
@mch: ты прав, sizeof(Foo) 12 лет
@Broman: конечно, это неопределенное поведение, вопрос в том, что на самом деле происходит? Почему это все еще работает на этой архитектуре?
Что касается того, почему это работает независимо от выравнивания: По-видимому, некоторые ядра ARM поддерживают некоторые невыровненные доступы.
Вы можете столкнуться с проблемами с порядком байтов и выравниванием на некоторых платформах. Другие будут работать. Ни один из них не гарантирует работу. Одним из решений является чтение значений в локальную переменную unsigned int и присвоение отсканированного значения члену структуры, возможно, после проверки диапазона значений, предназначенных для переменных uint8_t (и желательно после проверки результата из sscanf() каждый раз и работы с любые неудачи).
"а как насчет устаревшего компилятора C89?" --> C89 не имеет ` uint8_t, uint32_t`, поэтому сканирование является лишь одной из многих проблем.
@chux, очевидно, вопрос был не в этом. Во всяком случае, я заменил фиксированные целочисленные типы из C99 на простые старые типы.
Плохой этикет SO для фундаментального изменения вопроса в качестве ответа. Откат.
@chux: это было совершенно законное редактирование незначительной детали. Я повторю редактирование.
Если вы считаете, что редактирование было незначительным, вы можете пойти любым путем, в том числе оставить его исходным. Тем не менее, все 3 ответа уменьшаются в цене, поскольку все они относятся к исходным типам с фиксированной шириной.
@chux обратите внимание, что ваш собственный ответ в любом случае ОТ: вопрос явно «какое неопределенное поведение вызывает работу этой очевидной ошибки»; вы ответили: «Код UB, поскольку спецификаторы сканирования не соответствуют аргументам».





В данном случае с точки зрения указателя ничего, так как на всех современных машинах указатели одинаковы для всех типов.
Но поскольку вы используете неправильные форматы, scanf будет записывать за пределы памяти, выделенной для переменных, и это неопределенное поведение.
int, которые не существуют для значений char, и исходное значение char не выровнено должным образом.
@AndrewHenle Даже если есть дополнение (и это связано с реализацией), это все равно UB. Просто представьте сборку, в которой упакованы структуры.
Bonus question: what is the cleanest correct way to do this? I know that now we have the %hhu format specifier (apparently introduced by C++11), but what about a legacy C89 compiler?
Заголовок <stdint.h> и его типы были введены в C99, поэтому компилятор C89 не поддерживает их, кроме как в качестве расширения.
Правильный способ использования семейств функций *scanf() и *printf() с различными типами фиксированной или минимальной ширины — использовать макросы из <inttypes.h>. Например:
#include <inttypes.h>
#include <stdlib.h>
#include <stdio.h>
int main(void) {
int8_t foo;
uint_least16_t bar;
puts("Enter two numbers");
if (scanf("%" SCNd8 " %" SCNuLEAST16, &foo, &bar) != 2) {
fputs("Input failed!\n", stderr);
return EXIT_FAILURE;
}
printf("You entered %" PRId8 " and %" PRIuLEAST16 "\n", foo, bar);
}
В качестве дополнительного примечания для C89: просто используйте временную переменную с правильным типом. Или просто используйте strtoul, поскольку использование sscanf кажется бесполезным.
Прежде всего, это, конечно, вызывает Undefined Behavior.
Но такого рода ужасы были довольно распространены в старом коде, где язык C использовался как язык ассемблера более высокого уровня. Итак, вот 2 возможных поведения:
uint8_t получат младший значащий байт 32-битного значения, а байты заполнения будут обнулены (я предполагаю, что программа не пытается сохранить значение больше 255 в uint8_t)scanf записывать в неправильно выровненные переменные. Младший значащий байт значения, прочитанного для qux, правильно перейдет в qux, а следующие 3 нулевых байта сотрут xyz и etc. В следующей строке xyz получает свое значение, а etc получает еще один 0 байт. И, наконец, etc получит свое значение. Это могло быть довольно распространенным взломом в начале 80-х на машине типа 8086.Для переносимого способа я бы использовал временное целое число без знака:
uint32_t u;
sscanf(s1, "%u", &(foo->bar));
sscanf(s2, "%u", &u);
foo->qux = (uint8_t) u;
sscanf(s3, "%u", &u);
foo->xyz = (uint8_t) u;
sscanf(s4, "%u", &(foo->etc));
и доверяйте компилятору генерировать код столь же эффективно, как и способ ужастик.
uint32_t u;sscanf(s2, "%u", &u); имеет смысл, когда unsigned 32-битный. Однако C этого не требует.
Код OP — UB, так как спецификаторы сканирования не соответствуют аргументам.
cleanest correct way to do this?
Очиститель
#include <inttypes.h>
void horror1(const char* s1, const char* s2, const char* s3, const char* s4, Foo* foo) {
sscanf(s1, "%" SCNu32, &(foo->bar));
sscanf(s2, "%" SCNu8, &(foo->qux));
sscanf(s2, "%" SCNu8, &(foo->xyz));
sscanf(s1, "%" SCNu32, &(foo->etc));
}
Самый чистый
При желании добавьте дополнительную обработку ошибок.
void horror2(const char* s1, const char* s2, const char* s3, const char* s4, Foo* foo) {
foo->bar = (uint32_t) strtoul(s1, 0, 10);
foo->qux = (uint8_t) strtoul(s1, 0, 10);
foo->xyz = (uint8_t) strtoul(s1, 0, 10);
foo->etc = (uint32_t) strtoul(s1, 0, 10);
}
Действительно ли
xyzсоответствует 32-битному формату? В противном случаеsscanf(s3, "%u", (uint32_t*) &(foo->xyz));будет иметь выровненный доступ для записи.