-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 是一种众所周知的攻击类型,它基于您在问题中展示的问题。
给定以下代码,参数 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 是一种众所周知的攻击类型,它基于您在问题中展示的问题。