【问题标题】:What is forbidden after pointer-casting a big type to a smaller type in C在C中将大类型指针转换为较小类型后禁止什么
【发布时间】:2021-11-29 09:39:45
【问题描述】:

假设我有一个更大的类型。

uint32_t big = 0x01234567;

那么对于(char*)&big,指针转换后解释为char类型怎么办?

  1. (char*)&big 的地址转移(char*&big)+1(char*&big)+2 等是否是未定义的行为?
  2. 对于 shiftedit (char*)&big+1,这是否是未定义的行为?就像下面的例子。我认为这个例子应该是一个未定义的行为,因为在转换为(char*) 之后,我们将视线限制在char 类型的指针上,我们不应该访问,甚至不应该更改这个范围之外的值。
uint32_t big = 0x01234567;
*((char*)&big + 1) = 0xff;
printf("%02x\n\n\n", *((char*)&big+1));
printf("%02x\n\n\n", big);

(这通过了我的 Visual C++ 编译器。顺便说一句,我想问一个分叉的问题,为什么在这个例子中第一个 printf 给出 ffffffff?不应该是 ff 吗?)

  1. 我见过这样的代码。当我需要完成类似的任务时,我通常会这样做。这是UB还是不是?为什么或者为什么不?实现这一目标的标准方法是什么?
uint8_t catcher[8] = { 0 };
uint64_t big = 0x1234567812345678;
memcpy(catcher, (uint8_t*)&big, sizeof(uint64_t));

【问题讨论】:

    标签: c pointers casting


    【解决方案1】:

    那么对于(char*)&big,指针转换后解释为char类型怎么办?

    如果 char 是 8 位,这是大多数现代 C 实现中的情况,那么 uint32_t big 中有四个字节,您可以对从 (char *) &big + 0 到 @987654326 的地址进行算术运算@。您还可以读取和写入从(char *) &big + 0(char *) &big + 3 的字节,这些字节将访问big 表示中的各个字节。虽然算术被定义为在(char *) &big + 4 下工作,但这只是一个端点。那里没有定义的字节,您不应该使用该地址来读取或写入任何内容。

    1. (char*)&big 的地址转移(char*&big)+1(char*&big)+2 等是否是未定义的行为?

    这些是加法,而不是移位,语法是(char *) &big + 1,而不是(char*&big)+1。算术是为从 +0 到 +4 的偏移量定义的。

    1. 对于 shiftedit (char*)&big+1987654336@,这是一种未定义的行为吗?

    允许使用指向char 的指针读取和写入big 中的字节。这是字符类型的特殊规则。通常,不应使用不相关的类型访问对象的字节。例如,无法使用 int 类型访问 float 对象。但是,字符类型是特殊的;您可以使用字符类型访问任何对象的字节。

    但是,最好使用unsigned char,因为它可以避免有符号值的复杂性。

    1. 我见过这样的代码。

    允许使用memcpy 读取或写入对象的字节。 memcpy 被定义为像复制字符一样工作。

    请注意,虽然访问对象的字节是由 C 标准定义的,但字节表示值的方式部分是由实现定义的。不同的 C 实现可能对对象中的字节使用不同的顺序,并且可能存在其他差异。

    顺便说一句,我想问一个分叉的问题,为什么在这个例子中第一个printf 给出ffffffff?不应该是ff吗?

    在您的 C 实现中,char 是有符号的,可以表示从 -128 到 +127 的值。在 *((char*)&big + 1) = 0xff; 中,0xff 是 255,太大而无法放入 char。它以实现定义的方式转换为char 值。您的 C 实现将其转换为 -1。 (-1 的 8 位二进制补码表示,即 11111111 位,使用与 255 的二进制表示相同的位,同样是 11111111 位。)

    然后printf("%02x\n\n\n", *((char*)&big+1)); 将此值-1 传递给printf。由于它是char,因此将其提升为int 以传递给printf。这会产生相同的值 -1,但它有 32 位,11111111111111111111111111111111。然后你传递一个int,但printf 期望unsigned int 对应%02x。这种行为不是由 C 标准定义的,但是您的 C 实现读取 32 位,就好像它们是 unsigned int。作为unsigned int,32 位 1111111111111111111111111111111 代表值 4,294,967,295 或 0xffffffff,这就是 printf 打印的内容。

    您可以使用printf("%02hhx\n\n\n", * ((unsigned char *) &big + 1)); 打印正确的值。作为 unsigned char,位 11111111 代表 255 或 0xff,并将其转换为 int 会产生 255 或 0x000000ff

    【讨论】:

    • 非常感谢!它真的帮助我澄清了很多事情。顺便说一句,我想询问有关编译器/运行时方面的详细信息。当我写(char*)&big+6时,编译器(或操作系统)是否会首先计算地址,并检查相应的内存位置是否归我所有,然后决定我是否能够编辑那里的值?还是我一写(char*)&big+6,编译器或操作系统就知道肯定超出了我合法拥有的范围?
    • 我的意思是,有人(某些机器)可以知道 n 的极限是多少,这样(char*)&big+n 是可读的,这似乎很奇怪,因为在使用(char*) 投射&big 之后,最初&big 是一个更大的类型(并且有 4 个字节)的信息应该被遗漏。只有当机器实际计算(char*)&big+6并且要去那个地址时,它才能检测到程序员的访问可能是有问题的。
    • @Eric:C 标准没有定义(char *) &big + 6 的行为(假设big 只有四个字节)。对于某些类型的错误,C 标准可能要求编译器发出诊断消息或要求库例程提供错误代码。但是当它根本没有定义行为时,对编译器或程序的执行没有任何要求。一些编译器可能会在编译程序时检测到(char *) &big + 6 是一个错误。在大多数 C 实现中,计算地址本身不会导致问题……
    • … 但是,当使用地址读取或写入数据时,处理器可能会检测到问题。现代操作系统以各种方式将部分虚拟地址空间标记为可用或不可用——可以读取、可以执行(可以从该地址获取要执行的指令)、可以写入。如果你从一个没有被标记为可能读的地址中读取一个字节,或者向一个没有被标记为可能被写的地址写入一个字节,那么处理器就会产生一个陷阱……
    • ... 但是,由于我们知道&big 是一个可读和可写的地址,所以&big + 6 成为受保护地址的唯一方法是big 恰好位于保护更改的边界上.最常见的是,big 将是内存中存储其他内容的区域的一部分,因此它不太可能是该区域中的最后一项,靠近保护更改的地方。因此,处理器内存保护不可能捕捉到这样的寻址小错误。
    【解决方案2】:

    对于variadic functions(如printf),所有参数都经历default argument promotion其中promotes较小的整数类型到int

    如果较小的类型是有符号的,则此转换将包括 符号扩展,因此该值保持其值。

    因此,如果char 是一个有符号类型(由实现定义),其值为-1,那么它将提升为int-1。你看到的就是这个。

    如果您想打印较小的类型,您首先需要转换为正确的类型 (unsigned char),然后使用正确的格式(例如 %hhx 用于打印 unsigned char 值)。

    【讨论】:

      猜你喜欢
      • 2021-09-20
      • 2011-10-08
      • 1970-01-01
      • 1970-01-01
      • 2020-11-17
      • 1970-01-01
      • 1970-01-01
      • 2023-04-09
      相关资源
      最近更新 更多