-m32 在参数为 unsigned long long 时给出无法解释的问题

-m32 gives unexplained issue when argument is unsigned long long

给定以下代码,参数 printf 语句错误 'a':

#include <stdio.h>
void call(unsigned long long a, int b)
{
    printf("%lu,%d\n",a,b);
    printf("%llu,%d\n",a,b);
}
void main()
{
    call(0,1);
}

当你正常编译时,你会得到:

$ gcc m32.c
m32.c: In function ‘call’:
m32.c:4:12: warning: format ‘%lu’ expects argument of type ‘long unsigned int’, but argument 2 has type ‘long long unsigned int’ [-Wformat=]
     printf("%lu,%d\n",a,b);
            ^
$ ./a.out
0,1
0,1

但是当你用 -m32 编译它时,你会得到以下输出:

$ gcc -m32 m32.c
m32.c: In function ‘call’:
m32.c:4:12: warning: format ‘%lu’ expects argument of type ‘long unsigned int’, but argument 2 has type ‘long long unsigned int’ [-Wformat=]
     printf("%lu,%d\n",a,b);
            ^
$ ./a.out
0,0
0,1

显然,第一个 printf 是错误的,但是如您所见,在 printf 之后,打印中的第二个参数是错误的,而我不希望发生这种情况。我无法解释。这怎么可能?

你最好停在这里

 printf("%lu,%d\n",a,b);

提供的参数与转换说明符的预期类型不匹配会导致 undefined behavior。之后,无论发生什么,都没有人负责。

引用 C11,章节 §7.21.6.1

[...] If any argument is not the correct type for the corresponding conversion specification, the behavior is undefined.

理解你在说什么很重要。

printf("%lu,%d\n",a,b);

printf()解释为:

  • 有个long unsigned
  • 有一个int

在这种情况下,你在撒谎 - 这不是真的。 在您的情况下,这特别糟糕的是您的系统在 34 位和 64 位之间更改 unsigned long 的大小(就像我的一样)。

#include <stdio.h>

int main(void) {
        printf("sizeof(unsigned long): %zd\n", sizeof(unsigned long));
        printf("sizeof(unsigned long long): %zd\n", sizeof(unsigned long long));
        return 0;
}
$ gcc ll.c -o ll -m32 && ./ll
sizeof(unsigned long): 4
sizeof(unsigned long long): 8
$ gcc ll.c -o ll && ./ll
sizeof(unsigned long): 8
sizeof(unsigned long long): 8

所以,是的,它适用于 64 位(错误地),但对于 32 位 printf() 从堆栈中取出错误大小的值。

确保格式字符串与参数匹配非常重要。


我们可以对此进行测试(忽略那些有用的警告...):

#include <stdio.h>

int main(void) {
    unsigned long long x;
    int y;

    x = 0x8A7A6A5A4A3A2A1ALLU;
    y = 0x4B3B2B1B;

    printf("%lx - %x\n", x, y);
    printf("%llx - %x\n", x, y);

    return 0;
}
$ gcc ll.c -o ll -m32 && ./ll
ll.c: In function ‘main’:
ll.c:10:2: warning: format ‘%lx’ expects argument of type ‘long unsigned int’, but argument 2 has type ‘long long unsigned int’ [-Wformat=]
  printf("%lx - %x\n", x, y);
  ^
4a3a2a1a - 8a7a6a5a
8a7a6a5a4a3a2a1a - 4b3b2b1b
$ gcc ll.c -o ll && ./ll
ll.c: In function ‘main’:
ll.c:10:2: warning: format ‘%lx’ expects argument of type ‘long unsigned int’, but argument 2 has type ‘long long unsigned int’ [-Wformat=]
  printf("%lx - %x\n", x, y);
  ^
8a7a6a5a4a3a2a1a - 4b3b2b1b
8a7a6a5a4a3a2a1a - 4b3b2b1b

可以看到在32位的运行中,x的值被break分割了! printf() 已经采用了 'first' 32 位,然后是 'next' 32 位,而实际上我们为它提供了一个 64 位值 - 字节顺序使它变得有点更混乱。


如果您想对可变大小进行规定,请在此处查看我的回答:

@Attie 回答得好!另外,因为你触发了我低级的热情:)我会尝试从另一个角度来回答这个问题。

如您所知,在 x86 架构 中,函数参数通过 stack 发送,而在 x64 架构 函数参数通过寄存器发送(按顺序为 RDI、RSI、RDX、RCX、R8、R9)。

因此,与您的问题相关的重要一点是,当您编译 32 位时,printf 调用的参数是通过堆栈发送的。这是您的堆栈在两次 printf 调用之前的样子:

堆栈中的每个矩形块都由一个 32 位数字表示(因为您处于 x86 体系结构中)。您想发送一个 64 位数字作为 printf 的第一个参数!为此,编译器将 unsigned long long 数字拆分为两个 32 位部分,并将它们分别压入堆栈。这就是为什么您在堆栈中得到两个零以及整数中的一个值的原因。

现在分析一下printf的第一个调用。

0,0

由于它具有 "%lu,%d\n" 格式说明符,因此它必须从堆栈中获取一个无符号长整型和一个整型。 %lu在x86架构中是32位的,所以printf只从栈中取出一个块。在此之后,为整数再取一个块(因为我们只 "consumed" %lu 的两个零之一,我们将为 %d 得到另一个零)。

第二次调用printf输出正常值。

0,1

此调用是使用 "%llu,%d\n" 格式说明符完成的。 %llu 在 x86 体系结构中是 64 位的,因此 printf 从堆栈中取出 TWO 块,从而打印一个零。在此之后,它又从堆栈中取出一个块用于整数(这是具有一个值的块)。

您必须非常小心发送给 printf 函数的字符串格式说明符! format string attack 是一种众所周知的攻击类型,它基于您在问题中展示的问题。