为什么与 printf 未定义行为中的转换说明符不匹配的参数?
Why are arguments which do not match the conversion specifier in printf undefined behavior?
在 C (n1570 7.21.6.1/10) 和 C++(通过包含 C 标准库)中,向 printf 提供类型与其转换规范不匹配的参数是未定义的行为。一个简单的例子:
printf("%d", 1.9)
格式字符串指定一个int,而参数是一个浮点类型。
这个问题的灵感来自一位用户的问题,该用户遇到了遗留代码,其中包含大量转换不匹配,显然没有任何危害,请参见。 undefined behaviour in theory and in practice.
一开始仅仅声明 UB 格式不匹配似乎有些过激。很明显 output 可能是错误的,这取决于确切的不匹配、参数类型、字节顺序、可能的堆栈布局和其他问题。正如那里的一位评论员指出的那样,这也延伸到后续(甚至之前?)的论点。但这远非一般的UB。就个人而言,除了预期的错误输出之外,我从未遇到过任何其他问题。
为了大胆猜测,我会排除对齐问题。我可以想象的是,提供一个格式字符串使 printf 期望大数据和小实际参数可能会让 printf
读取堆栈之外的内容,但我缺乏对 var args 机制和特定 printf 实现细节的更深入了解来验证那。
我快速浏览了 printf sources,但它们对不经意的人来说相当不透明 reader。
因此我的问题是:printf
中转换说明符和参数不匹配的具体危险是什么?
printf
只有在正确使用的情况下才能按照标准描述的那样工作。如果使用不当,行为是未定义的。为什么标准要定义错误使用时会发生什么?
具体来说,在某些体系结构中,浮点参数在不同的寄存器中传递给整数参数,因此在 printf
内部,当它试图找到与格式说明符匹配的 int
时,它会在相应的寄存器。由于这些细节超出了标准的范围,因此除了说它未定义外,没有办法处理这种不当行为。
举例说明它可能出错的严重程度,使用格式说明符 "%p"
但传递浮点类型可能意味着 printf
尝试从寄存器或堆栈读取指针location 尚未设置为有效值并且可能包含陷阱表示,这将导致程序中止。
仅举您的示例:假设您的体系结构的过程调用标准规定浮点参数在浮点寄存器中传递。但是 printf
认为你传递的是一个整数,因为 %d
格式说明符。所以它期望在调用堆栈上有一个参数,但它不存在。现在什么都有可能发生。
任何 printf
format/argument 不匹配都会导致错误输出,所以一旦你这样做就不能依赖任何东西。很难说除了垃圾输出之外哪个会产生可怕的后果,因为它完全不依赖于你正在编译的平台的细节以及 printf
实现的实际细节。
将无效参数传递给具有 %s
格式的 printf
实例会导致无效指针被取消引用。但是 更简单 类型的无效参数(例如 int
或 double
可能会导致对齐错误并产生类似的后果。
首先我会要求您注意 long
对于 64 位版本的 OS X、Linux、BSD 克隆来说是 64 位的,以及各种 Unix 风格(如果您还不知道的话)。然而,64 位 Windows 将 long
保持为 32 位。
就其转换规范而言,这与 printf()
和 UB 有什么关系?
在内部,printf()
将使用 va_arg()
宏。如果你在 64 位 Linux 上使用 %ld
并且只传递一个 int
,其他 32 位将从相邻的内存中检索。如果您使用 %d
并在 64 位 Linux 上传递 long
,其他 32 位仍将位于参数堆栈中。换句话说,转换规范表明类型(int
,long
,随便)到va_arg()
,对应类型的大小决定了[=14=的字节数] 调整其参数指针。虽然它只能在 Windows 上运行,因为 sizeof(int)==sizeof(long)
,但将它移植到另一个 64 位平台可能会造成麻烦,尤其是当您有 int *nptr;
并尝试使用 %ld
时*nptr
。如果您无权访问相邻的内存,则可能会出现段错误。所以可能的具体情况是:
- 相邻的内存被读取,并且从那一点开始输出混乱
- 尝试读取相邻内存,由于保护机制出现段错误
long
和 int
的大小相同,所以可以正常工作
- 获取的值被截断,从那一点开始输出变得混乱
我不确定在某些平台上是否存在对齐问题,但如果是,则取决于传递函数参数的实现。一些 "intelligent" 特定于编译器的 printf()
和一个简短的参数列表可能会完全绕过 va_arg()
并将传递的数据表示为字节串而不是使用堆栈。如果发生这种情况,printf("%x %lx\n", LONG_MAX, INT_MIN);
有三种可能性:
long
和 int
的大小相同,所以可以正常工作
ffffffff ffffffff80000000
打印
- 程序因对齐错误而崩溃
至于为什么 C 标准说它会导致未定义的行为,它没有具体说明 va_arg()
是如何工作的,函数参数是如何在内存中传递和表示的,或者 [=16] 的显式大小=]、long
或其他原始数据类型,因为它不会不必要地限制实现。因此,无论发生什么,C 标准都无法预测。仅查看上面的示例就可以说明这一事实,我无法想象还有哪些其他实现可能完全不同。
一些编译器可能会以允许
要验证的参数类型;因为有一个程序陷阱不正确
使用可能比可能输出看似有效但错误的输出更好
信息,某些平台可能会选择这样做。
因为陷阱的行为超出了 C 标准的范围,所以任何动作
这可能会陷阱被归类为调用未定义的行为。
请注意,基于不正确格式的实现陷阱的可能性意味着即使在预期类型和实际传递的类型具有相同表示的情况下,行为也被视为未定义,除了相同级别的有符号和无符号数字是如果它们所持有的值在两者共有的范围内[即如果 "long" 包含 23,即使 "int" 和 "long" 的大小相同,它也可能输出为 "%lX" 而不是 "%X"。
另请注意,C89 委员会通过法令引入了一条规则,该规则一直保留到今天,其中规定即使 "int" 和 "long" 具有相同的格式,代码:
long foo=23;
int *u = &foo;
(*u)++;
调用未定义行为,因为它导致以类型 "long" 写入的信息被读取为类型 "int"(如果行为类型为 "unsigned int",行为也将是未定义的)。由于“%X”格式说明符会导致数据以 "unsigned int" 类型读取,因此将数据作为 "long" 类型传递几乎肯定会导致数据以 "long" 类型存储在某处,但随后读作 "unsigned int" 类型,这种行为很可能会违反上述规则。
在 C (n1570 7.21.6.1/10) 和 C++(通过包含 C 标准库)中,向 printf 提供类型与其转换规范不匹配的参数是未定义的行为。一个简单的例子:
printf("%d", 1.9)
格式字符串指定一个int,而参数是一个浮点类型。
这个问题的灵感来自一位用户的问题,该用户遇到了遗留代码,其中包含大量转换不匹配,显然没有任何危害,请参见。 undefined behaviour in theory and in practice.
一开始仅仅声明 UB 格式不匹配似乎有些过激。很明显 output 可能是错误的,这取决于确切的不匹配、参数类型、字节顺序、可能的堆栈布局和其他问题。正如那里的一位评论员指出的那样,这也延伸到后续(甚至之前?)的论点。但这远非一般的UB。就个人而言,除了预期的错误输出之外,我从未遇到过任何其他问题。
为了大胆猜测,我会排除对齐问题。我可以想象的是,提供一个格式字符串使 printf 期望大数据和小实际参数可能会让 printf
读取堆栈之外的内容,但我缺乏对 var args 机制和特定 printf 实现细节的更深入了解来验证那。
我快速浏览了 printf sources,但它们对不经意的人来说相当不透明 reader。
因此我的问题是:printf
中转换说明符和参数不匹配的具体危险是什么?
printf
只有在正确使用的情况下才能按照标准描述的那样工作。如果使用不当,行为是未定义的。为什么标准要定义错误使用时会发生什么?
具体来说,在某些体系结构中,浮点参数在不同的寄存器中传递给整数参数,因此在 printf
内部,当它试图找到与格式说明符匹配的 int
时,它会在相应的寄存器。由于这些细节超出了标准的范围,因此除了说它未定义外,没有办法处理这种不当行为。
举例说明它可能出错的严重程度,使用格式说明符 "%p"
但传递浮点类型可能意味着 printf
尝试从寄存器或堆栈读取指针location 尚未设置为有效值并且可能包含陷阱表示,这将导致程序中止。
仅举您的示例:假设您的体系结构的过程调用标准规定浮点参数在浮点寄存器中传递。但是 printf
认为你传递的是一个整数,因为 %d
格式说明符。所以它期望在调用堆栈上有一个参数,但它不存在。现在什么都有可能发生。
任何 printf
format/argument 不匹配都会导致错误输出,所以一旦你这样做就不能依赖任何东西。很难说除了垃圾输出之外哪个会产生可怕的后果,因为它完全不依赖于你正在编译的平台的细节以及 printf
实现的实际细节。
将无效参数传递给具有 %s
格式的 printf
实例会导致无效指针被取消引用。但是 更简单 类型的无效参数(例如 int
或 double
可能会导致对齐错误并产生类似的后果。
首先我会要求您注意 long
对于 64 位版本的 OS X、Linux、BSD 克隆来说是 64 位的,以及各种 Unix 风格(如果您还不知道的话)。然而,64 位 Windows 将 long
保持为 32 位。
就其转换规范而言,这与 printf()
和 UB 有什么关系?
在内部,printf()
将使用 va_arg()
宏。如果你在 64 位 Linux 上使用 %ld
并且只传递一个 int
,其他 32 位将从相邻的内存中检索。如果您使用 %d
并在 64 位 Linux 上传递 long
,其他 32 位仍将位于参数堆栈中。换句话说,转换规范表明类型(int
,long
,随便)到va_arg()
,对应类型的大小决定了[=14=的字节数] 调整其参数指针。虽然它只能在 Windows 上运行,因为 sizeof(int)==sizeof(long)
,但将它移植到另一个 64 位平台可能会造成麻烦,尤其是当您有 int *nptr;
并尝试使用 %ld
时*nptr
。如果您无权访问相邻的内存,则可能会出现段错误。所以可能的具体情况是:
- 相邻的内存被读取,并且从那一点开始输出混乱
- 尝试读取相邻内存,由于保护机制出现段错误
long
和int
的大小相同,所以可以正常工作- 获取的值被截断,从那一点开始输出变得混乱
我不确定在某些平台上是否存在对齐问题,但如果是,则取决于传递函数参数的实现。一些 "intelligent" 特定于编译器的 printf()
和一个简短的参数列表可能会完全绕过 va_arg()
并将传递的数据表示为字节串而不是使用堆栈。如果发生这种情况,printf("%x %lx\n", LONG_MAX, INT_MIN);
有三种可能性:
long
和int
的大小相同,所以可以正常工作ffffffff ffffffff80000000
打印- 程序因对齐错误而崩溃
至于为什么 C 标准说它会导致未定义的行为,它没有具体说明 va_arg()
是如何工作的,函数参数是如何在内存中传递和表示的,或者 [=16] 的显式大小=]、long
或其他原始数据类型,因为它不会不必要地限制实现。因此,无论发生什么,C 标准都无法预测。仅查看上面的示例就可以说明这一事实,我无法想象还有哪些其他实现可能完全不同。
一些编译器可能会以允许 要验证的参数类型;因为有一个程序陷阱不正确 使用可能比可能输出看似有效但错误的输出更好 信息,某些平台可能会选择这样做。
因为陷阱的行为超出了 C 标准的范围,所以任何动作 这可能会陷阱被归类为调用未定义的行为。
请注意,基于不正确格式的实现陷阱的可能性意味着即使在预期类型和实际传递的类型具有相同表示的情况下,行为也被视为未定义,除了相同级别的有符号和无符号数字是如果它们所持有的值在两者共有的范围内[即如果 "long" 包含 23,即使 "int" 和 "long" 的大小相同,它也可能输出为 "%lX" 而不是 "%X"。
另请注意,C89 委员会通过法令引入了一条规则,该规则一直保留到今天,其中规定即使 "int" 和 "long" 具有相同的格式,代码:
long foo=23;
int *u = &foo;
(*u)++;
调用未定义行为,因为它导致以类型 "long" 写入的信息被读取为类型 "int"(如果行为类型为 "unsigned int",行为也将是未定义的)。由于“%X”格式说明符会导致数据以 "unsigned int" 类型读取,因此将数据作为 "long" 类型传递几乎肯定会导致数据以 "long" 类型存储在某处,但随后读作 "unsigned int" 类型,这种行为很可能会违反上述规则。