动态format的printf

动态format的printf

Tags
c++
va_list
可变参数
AI summary

动态format的Printf

因为工作上的需要,要实现一个动态format的printf,即format在运行时才能知道,参数按4字节对齐放在一块buffer,运行时可以知道参数的个数,以及所有参数的指针。
std::string foramt; std::vector<void*> args; // 想要的效果 my_printf(format, args);

想法一

参照printf的实现方式,手动解析format,自己实现一个printf的功能。不过leader说咱们不去做这个解析,容易出bug。

想法二

leader想要直接调用c printf,一把直接实现打印。但是这样有两个个问题:
  • printf是一个可变参数的函数,调用printf时,参数个数在编译期就确定了,但是我这里的format和参数个数都是需要在运行时才能获取到。
  • 和参数个数一样,调用printf时,参数类型也要在编译期就确定。
 

参数个数

于是想到规定printf参数的数量时有上限(比如32),通过一个switch表,根据参数的数量执行对应的printf。

参数类型

调用函数传参时,无非就是将对应参数压到栈上(或寄存器),然后被调用的函数通过一些手段从栈上获取参数的值,因此参数的类型或许没有那么大影响,并且printf支持的类型就只有那几种基本类型,我是不是能把所有参数都通过int64_t类型传进去?于是做了下实验。
#include <cstdio> #include <cstdint> int main() { char a = 'A'; short b = 55; unsigned int c = 1234; long long int d = 0x0fffffffffffffff; float e = 0.1234; printf("%c, %hd, %u, %lld, %f\n", a, b, c, d, e); printf("%c, %hd, %u, %lld, %f\n", *(int64_t*)(&a), *(int64_t*)(&b), *(int64_t*)(&c), *(int64_t*)(&d), *(int64_t*)(&e)); }
输出:
A, 55, 1234, 1152921504606846975, 0.123400 A, 55, 1234, 1152921504606846975, 0.123400
这个思路看起似乎是ok的,不过调整下%f的位置,就出现了问题:
printf("%f, %c, %hd, %u, %lld\n", *(int64_t*)(&e), *(int64_t*)(&a), *(int64_t*)(&b), *(int64_t*)(&c), *(int64_t*)(&d));
输出:
0.123400, $, 4161, 1090519095, 4683743848688518354
可以发现输出出现了错误,可是为什么呢,这和可变参数函数的实现方式以及我是跑在64位机器上有关系。

想法三,va_list

上面的思路走不通,于是去仔细了解了下可变参数函数的实现,准备从这里入手。

传参方式

首先咱们都知道,在32位机器上面,传参是通过栈传递的。
而到了64位机器上,查询资料后知道,前六个参数通过寄存器传递,剩下再通过栈传递。不过如果是这样的话,不能解释上面出现的问题。
先放着这个疑问不管,继续了解可变参数函数的实现,其中最关键的就是va_list这个结构。

va_list原理

参照在我的工作环境上,va_list的结构如下:
struct { unsigned int gp_offset; unsigned int fp_offset; void* overflow_arg_area; void* reg_save_area; }
如何确定这个结构,和其中字段的意义可以参照这篇文章。
这篇文章较为详细地解释了各个字段的意义,和va_list的工作原理。
因为fp_offset字段的存在,我猜测是不是在传参时,64位平台下,针对浮点类型的变量,会优先放到浮点寄存器里面,超过上限的浮点参数再放到栈上。这样就可以解释之前为什么%f换个位置结果就出错了。

构造va_list

知道va_list的工作原理后,就可以自己构造一个va_list了。
stackoverflow上已经这个问题了,也有人给出了一个可以运行的代码。
高赞回答的代码里,思路也很简单。
  1. 申请一块buffer作为overflow_area
  1. 把所有的参数都按4字节对齐放在overflow_area
  1. 设置gp_offset为48, fp_offset为304,这样就会直接从overflow_area开始取数据。

deafult argument promotions

够造好va_list后,直接调用vprintf,就能一把进行打印了。
vprintf(format, va_list);
但经过测试后,发现打印float类型时还是有问题,debug后发现数据也没问题。于是准备自己写个可变参数函数,手动解析下自己构造的va_list看看问题。然后在取float类型的数据的时候,编译器发出了警告。
va_arg(my_va_list, float);
💡
Warning. Second argument to 'va_arg' is of promotable type 'float'; this va_arg has undefined behaviour because arguments will be promoted to 'double'
警告说float会被promote成double,经过一番查询资料,了解到原来在可变参数函数传参时,float会先被promote为double。
其实不止float和被promote,bool, char, short都会被promote为int,只不过因为在构造va_list时,参数是按4字节对齐放置的,相当于进行了一次promote。而float和double的格式不一样,因此解析的时候也就不对了。
在构造va_list时,针对float类型的参数,手动promote后再放到overflow_area,问题就解决了。