Что произойдет, если аргументы, переданные в sscanf, будут преобразованы

Просматривая старый фрагмент кода, я наткнулся на какой-то ужас кодирования, подобный этому:

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, но это просто вводило в заблуждение и не по теме, и, кстати, исходный код, который я просматривал, использует свои собственные определения типов.

Действительно ли xyz соответствует 32-битному формату? В противном случае sscanf(s3, "%u", (uint32_t*) &(foo->xyz)); будет иметь выровненный доступ для записи.

mch 29.05.2019 09:21

@mch: ты прав, sizeof(Foo) 12 лет

lornova 29.05.2019 09:32

@Broman: конечно, это неопределенное поведение, вопрос в том, что на самом деле происходит? Почему это все еще работает на этой архитектуре?

lornova 29.05.2019 09:33

Что касается того, почему это работает независимо от выравнивания: По-видимому, некоторые ядра ARM поддерживают некоторые невыровненные доступы.

user694733 29.05.2019 10:27

Вы можете столкнуться с проблемами с порядком байтов и выравниванием на некоторых платформах. Другие будут работать. Ни один из них не гарантирует работу. Одним из решений является чтение значений в локальную переменную unsigned int и присвоение отсканированного значения члену структуры, возможно, после проверки диапазона значений, предназначенных для переменных uint8_t (и желательно после проверки результата из sscanf() каждый раз и работы с любые неудачи).

Jonathan Leffler 29.05.2019 10:29

"а как насчет устаревшего компилятора C89?" --> C89 не имеет ` uint8_t, uint32_t`, поэтому сканирование является лишь одной из многих проблем.

chux - Reinstate Monica 29.05.2019 13:17

@chux, очевидно, вопрос был не в этом. Во всяком случае, я заменил фиксированные целочисленные типы из C99 на простые старые типы.

lornova 29.05.2019 13:27

Плохой этикет SO для фундаментального изменения вопроса в качестве ответа. Откат.

chux - Reinstate Monica 29.05.2019 13:28

@chux: это было совершенно законное редактирование незначительной детали. Я повторю редактирование.

lornova 29.05.2019 13:35

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

chux - Reinstate Monica 29.05.2019 13:44

@chux обратите внимание, что ваш собственный ответ в любом случае ОТ: вопрос явно «какое неопределенное поведение вызывает работу этой очевидной ошибки»; вы ответили: «Код UB, поскольку спецификаторы сканирования не соответствуют аргументам».

lornova 29.05.2019 13:51
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
11
92
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

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

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

scanf будет писать за пределами выделенной памяти Если аппаратное обеспечение не имеет ограничений на выравнивание 32-битных значений int, которые не существуют для значений char, и исходное значение char не выровнено должным образом.
Andrew Henle 29.05.2019 09:25

@AndrewHenle Даже если есть дополнение (и это связано с реализацией), это все равно UB. Просто представьте сборку, в которой упакованы структуры.

0___________ 29.05.2019 09:35

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 кажется бесполезным.

user694733 29.05.2019 10:25
Ответ принят как подходящий

Прежде всего, это, конечно, вызывает Undefined Behavior.

Но такого рода ужасы были довольно распространены в старом коде, где язык C использовался как язык ассемблера более высокого уровня. Итак, вот 2 возможных поведения:

  • структура имеет 32-битное выравнивание. Все (довольно хорошо) на машине с прямым порядком байтов: члены uint8_t получат младший значащий байт 32-битного значения, а байты заполнения будут обнулены (я предполагаю, что программа не пытается сохранить значение больше 255 в uint8_t)
  • структура не имеет 32-битного выравнивания, но архитектура позволяет 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 этого не требует.
chux - Reinstate Monica 29.05.2019 13:14

Код 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);
}

Другие вопросы по теме