如果传递给sscanf的参数被强制转换,

时间:2019-05-29 07:15:17

标签: c casting scanf

在回顾旧代码的同时,我偶然发现了这样的一些编码恐怖:

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 intunsigned char而不是uint8_t,但这只是误导和超出主题,而且我查看的原始代码使用了自己的typedef。

4 个答案:

答案 0 :(得分:2)

在这种情况下,从指针的角度来看,没有什么比所有现代机器上的所有类型的指针都相同。

但是,因为使用了错误的格式-scanf会在分配给变量的内存之外进行写入,这是未定义的行为。

答案 1 :(得分:2)

  

奖金问题:最干净的正确方法是什么?我知道现在我们有了%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 :(得分:1)

首先,这当然会调用未定义的行为。

但是这种恐惧在旧代码中非常普遍,在旧代码中,C语言被用作高级汇编语言。因此,这里有两种可能的行为:

  • 该结构具有32位对齐方式。在一点字节序的机器上,一切都很好(相当好):uint8_t成员将收到32位值的最低有效字节,并且填充字节将被清零(我假设程序不会尝试存储值大于255成uint8_t
  • 该结构没有32位对齐方式,但是该体系结构允许scanf写入未对齐的变量。为qux读取的值的最低有效字节将正确进入qux,随后的三个零字节将擦除xyzetc。在下一行,xyz接收其值,etc再接收一个0字节。最后etc将获得其价值。在8086早期的80'型机器上,这可能是相当普遍的黑客行为。

以一种可移植的方式,我将使用一个临时的无符号整数:

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

并信任编译器生成与恐怖方式一样高效的代码。

答案 3 :(得分:0)

OP代码为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));
}

最干净

根据需要添加其他错误处理。

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