Что произойдет, если переданы аргументы sscanf - PullRequest
3 голосов
/ 29 мая 2019

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

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

Ответы [ 4 ]

2 голосов
/ 29 мая 2019

Бонусный вопрос: какой самый чистый и правильный способ сделать это? Я знаю, что теперь у нас есть спецификатор формата% hhu (по-видимому, представленный в C ++ 11), но как насчет устаревшего компилятора C89?

Заголовок <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);
}
2 голосов
/ 29 мая 2019

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

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

1 голос
/ 29 мая 2019

Прежде всего, это, конечно, вызывает неопределенное поведение.

Но такого рода ужасы были довольно распространены в старом коде, где язык 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));

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

0 голосов
/ 29 мая 2019

Код операции - UB, так как спецификаторы сканирования не совпадают с аргументами.

Самый правильный правильный способ сделать это?

Очиститель

#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));
}

Cleanest

При необходимости добавьте дополнительную обработку ошибок.

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);
}
...