与printf()和C参数传递有关的一个小难题

2021-01-01 17:19:29

布鲁斯·道森(Bruce Dawson)在《轻松的人–公开中的三个漏洞》中给我们带来了一个C难题:

inprintf格式的可变参数意味着很容易获得类型不匹配。实际结果差异很大:

printf(“ 0x%08lx”,p); //将指针打印为int –在64位上截断或更差

printf(“%d,%f”,f,i); //交换float和int –可以打印废话,或者可以实际工作(!)

printf(“%s%d”,i,s); //交换string和int的顺序–可能会崩溃

我不得不考虑一下,然后我意识到了它为什么起作用以及如何起作用(以及为什么类似的整数与浮点参数混淆也可以用于其他函数,甚至是带有固定参数列表的函数)。结果是,在某些ABI中,参数在寄存器中传递(至少早期参数,在用完寄存器之前),并且浮点参数在整数(和指针)之外的其他寄存器中传递。即使对于采用可变参数并使用stdarg宏逐步遍历它们的函数,也是如此(或者至少可以,取决于ABI)。

由于浮点和非浮点参数在不同的寄存器集中传递,因此重要的不是参数的总顺序,而是浮点或非fp参数的顺序。因此在这里,无论在何处%f #39;格式为printf时,它总是使printf()获得第一个浮点参数,永远不能将其与整数参数混淆。同样,第一个' d'导致printf()查找第二个非fp参数,而不管其在参数顺序中的位置;它可能在几个浮点参数的末尾并且仍然有效。

('%d'使printf()寻找第二个非fp参数,因为第一个是格式字符串。在ABI中,指针在与整数分开的地方传递指针,但它仍然可以解决,因为现在第一个&#39%d'将在寻找第一个整数参数。)

使用godbolt.org的出色服务,我们可以在一个非常小的示例中看到这种在64位x86上不起作用的情况(我使用了一个非常小的示例和适当的优化级别来获得清晰的最小汇编代码)。浮点参数在xmm0中传递,而格式字符串和整数参数分别在edi和esi中传递(我不知道eax在做什么,但它可能与ABI有关。)在64位ARM v8(akaAarch64)上也发生了类似的事情,就像我们在Godbolt上看到的相同示例onAarch64一样。

(基于此页面,Aarch64 x0和w1位于同一组寄存器中。显然d0是第一个浮点寄存器的64位版本,从这里开始[pdf]。我最终将所有这些查找为确保我了解Aarch64调用中发生了什么,所以我不妨在这里写下来。)

由于指针和整数通常在同一组寄存器中传递(至少在64位x86和Aarch64上),因此我们也可以看到为什么第三个示例很可能失败。由于两种参数类型都使用相同的寄存器集,因此可以将整数参数用作指针参数,可能会出现分段错误。同样,我们可以预测到' printf("%s%f&#34 ;, f,s);'可能会工作。

PS:这种混淆可能会在平台上使用CABI的任何一种使用这种对寄存器进行拆分的语言出现(尽管许多语言都可以防止这种自变量类型的混淆)。著名的是,Go当前将所有参数传递给堆栈(从Go 1.15开始,不久成为Go 1.16)。