【问题标题】:Why does scanf("%d") work with an 8-bit datatype casted to an int*, but printf does not?为什么 scanf("%d") 可以处理转换为 int* 的 8 位数据类型,但 printf 不能?
【发布时间】:2021-11-09 06:15:53
【问题描述】:

我试图通过将数据类型隐藏在void* 后面并将其转换为%d 的含义(即, int*):

#include <stdio.h>
#include <stdint.h>

int main()
{
    int8_t a, b;
    void *v[2] = { &a, &b };
    
    sscanf("-111,9\n", "%d,%d", (int*)v[0], (int*)v[1]);
    printf("Works  : %d, %d\n", a, b);
    printf("Doesn't: %d, %d\n\n", *(int*)v[0], *(int*)v[1]);
    return 0;
}

这是输出:

Works  : -111, 9
Doesn't: 0, 9

问题:

  1. 为什么scanf()ab 的直接printf 验证,在转换为int* 时可能读入8 位类型?不应该超限吗?
  2. 相反,为什么 printf() 无法打印被取消引用为 *(int*)v[0] 的 8 位类型,而 scanf 可以读入它们?
  3. 由于格式说明符无疑是不够的,因此是否有一些编译器时间魔法可以告诉 scanf/printf 数据类型是什么?

我知道这段代码可能是错误的,但我仍然对示例背后的细节感到好奇。

感谢您的帮助!

【问题讨论】:

  • scanf 不知道它的参数是什么类型:stackoverflow.com/questions/18203636/…
  • @Dai,由于取消引用,printf 核心转储,我认为这是正确的行为,因为 char 太小而无法读取为超出堆栈的 int,这可能是您看不到的原因它。但是scanf如何在不破坏堆栈的情况下将32位int(“%d”)写入8位类型?
  • 你得到了 UB,因为 scanf 在对象的范围之外写入
  • 这是未定义的行为,因此没有必要推测它。
  • "sscanf("-111,9\n", "%d,%d", &a, &b); 不会给出编译器警告," 如果启用编译器警告,它会:@ 987654322@.

标签: c pointers printf scanf


【解决方案1】:

TL;DR:不保证执行未定义行为会导致崩溃或诊断。它可能看起来可以工作。

为什么 scanf() 可以在转换为 int* 时读入 8 位类型,并由 a 和 b 的直接 printf 验证?不应该超限吗?

它确实溢出了——你正在(可能)将 4 字节的值写入 1 字节的空间,所以你正在破坏后面的 3 个字节。问题是,像这样的超出范围的写入是未定义的行为,它可能会崩溃,也可能不会崩溃,并且可能看起来有效。很可能他们只是在某处损坏了某些东西,这会导致一些后来的代码神秘地崩溃,这很可能是这里发生的事情(printf 调用崩溃或行为不端,因为 v 数组中的数据被 scanf 调用损坏了)。

如果我注释掉第二行 printf 并在 linux 上使用 gcc 编译它,它会给我:

$ ./test
Works  : -111, 9
*** stack smashing detected ***: ./test terminated
Aborted (core dumped)

这是完全一致的——带有不正确指针的 scanf 会导致未定义的行为,直到后面的代码尝试做某事(在这种情况下,当 main 返回并尝试清理其堆栈帧时)才会出现。

【讨论】:

    【解决方案2】:

    ab 的直接 printf 验证,为什么 scanf() 在转换为 int* 时可以读入 8 位类型?不应该超限吗?

    但它确实溢出了,ab 最终包含正常值的事实并不是任何一种“验证”!

    我这样修改了你的程序:

    int8_t x1 = 11;
    int8_t x2 = 22;
    int8_t x3 = 33;
    int8_t a;
    int8_t x4 = 44;
    int8_t x5 = 55;
    int8_t x6 = 66;
    int8_t b;
    int8_t x7 = 77;
    int8_t x8 = 88;
    int8_t x9 = 99;
    void *v[2] = { &a, &b };
    
    printf("before: %d %d %d %d %d %d %d %d %d\n", x1, x2, x3, x4, x5, x6, x7, x8, x9);
    sscanf("-111,9\n", "%d,%d", (int*)v[0], (int*)v[1]);
    printf("Works  : %d, %d\n", a, b);
    printf(" after: %d %d %d %d %d %d %d %d %d\n", x1, x2, x3, x4, x5, x6, x7, x8, x9);
    

    当我运行它时,我得到了这个输出:

    before: 11 22 33 44 55 66 77 88 99
    Works  : -111, 9
     after: -1 -1 -1 0 0 0 77 88 99
    

    毫不奇怪,大部分x 都被砸烂了。 (现在,不能保证编译器会一致地布置这些变量,所以这不是唯一可能的结果,但它非常清楚地表明,正如预期的那样,正在发生一些破坏。)

    【讨论】:

      【解决方案3】:

      上面的cmets回答了这个问题,这里是总结:

      1. scanf doesn't know what the types of its arguments are. - @Dia
      2. sscanf("-111,9\n", "%d,%d", &a, &b); does give a compiler warning if you enable compiler warnings.@RaymondChen
      3. 程序员有责任确保格式说明符与参数匹配。如果不是,则会出现未定义的行为。 - @GarrGodfrey
      4. “但是 scanf 怎样才能在不破坏堆栈的情况下将 32 位 int ("%d") 写入 8 位类型?”要回答这个问题,您需要查看汇编代码。确定堆栈的布局方式,看看什么东西被破坏了。因为有些东西正在被砸碎,但它一定不是什么重要的东西。使用 -S 编译以查看程序集,或使用调试器检查程序集。 – @user3386109

      【讨论】:

        【解决方案4】:

        我试图通过将数据类型隐藏在void* 后面并将其转换为%d 的含义(即, int*)

        阅读规范怎么样?或者至少是手册页?尽管对于实验有一些话要说,但仅凭这一点,你所能期望的最好的就是了解你的特定实现如何做特定的事情。依靠良好的文档作为实验的基础,可以让您站稳脚跟。

        文档将支持以下结论:scanf 完全依赖出现在其格式字符串中的字段指令来判断第二个和后续参数的类型。如果您传递不正确类型的参数,则会导致未定义的行为。 scanf 期望与 %d 指令对应的参数是 int *,并且就语言规范而言,void * 不会这样做,更不用说指向除 int 之外的某些完整类型的指针了.

        现在,给定 C 实现提供的所有对象指针类型具有相同的大小和表示形式是相当普遍的,因此通过强制转换在它们之间进行转换不会影响值的表示形式。在这种情况下,您的演员表本身虽然不正确,但可能不会对scanf 造成实际问题。 (但它可能,原因可能是晦涩难懂或神秘的。这就是 UB 的本质。)

        然而,事实是指向的对象不是int,也不是与int 类型兼容的对象,所以如果scanf 尝试通过指针访问该对象,则会发生未定义的行为也是这个原因。这违反了“严格的别名规则”。与演员表不同,这个很可能在实践中引起可观察到的不当行为。

        1. 为什么scanf()ab 的直接printf 验证,在转换为int* 时可能读入8 位类型? 不应该超限吗?

        谁说它不会溢出?您的程序具有未定义的行为。这可以表现为做你期望的事情,或者表现出这样做。在这种特殊情况下,我倾向于猜测做正确事情的外观部分取决于您读取变量的顺序以及(我推断)您使用的是小端计算机这一事实,例如基于英特尔的。

        1. 反之,为什么printf()在scanf可以读入的情况下,无法打印被解引用为*(int*)v[0]的8位类型 他们?

        您通过访问ab 再次调用未定义的行为,就好像它们是ints,而实际上它们不是。未定义的行为不必是一致的。或者它可能与在 C 语言级别不体现的结构和行为一致。这不是语言欠你解释的东西:这就是“未定义”的意思。

        1. 是否有一些编译器时的魔法可以告诉 scanf/printf 数据类型是什么,因为格式说明符无疑是 不够?

        格式字符串是语言规范所要求的全部内容,并且足以告诉scanfprintf 会发生什么。仅仅让他们验证参数类型实际上是他们被告知期望的是不够的,但是如果他们有这样的能力,那么他们就不需要格式字符串来告诉他们关于首先是类型。提供与格式字符串匹配的参数是程序员的责任,这并不麻烦,因为程序员自己也提供格式字符串。在scanf 的情况下,提供scanf 可以在不违反严格别名规则的情况下使用的有效指针值也是程序员的责任。该语言指定了当您正确执行该操作时会发生什么;当您做错时,它不会承诺任何特定行为。

        【讨论】:

          猜你喜欢
          • 2012-07-18
          • 1970-01-01
          • 2014-03-06
          • 2016-06-12
          • 2014-07-31
          • 1970-01-01
          • 1970-01-01
          • 2022-07-07
          • 2011-11-20
          相关资源
          最近更新 更多