int printf(const char format, ...);
个中:1)formmat : 固定参数 2)… :表示可变参数
可变参数的实现最紧张最靠的是C措辞的指针操作。由于C措辞函数参数的入栈顺序是于参数的顺序是相反的(即C措辞函数末了一个参数最先入栈,第一个参数末了入栈),我们只要知道第一个参数的地址便可访问到剩余的其他参数。听说,在x86平台下,函数调用时参数通报是利用堆栈来实现的。在ARM平台下,函数参数的通报遵照ATPCS规则,个中可变参数函数参数通报规则如下:
1)当参数不超过4个时,可以利用寄存器R0~R3来通报参数;当参数超过4个时,可以利用数据栈来通报参数。

2)在参数通报时,将所有参数看作是存放在连续的内存字单元的数据。然后,然后依次将各个字数据传送到寄存器R0、R1、R2、R3中,如果参数多于四个,将剩余的字数据传送到数据栈中,入栈顺序于参数顺序相反。
例如,在ARM平台下,我们可以编写如下代码,通过反汇编不雅观察可变参数函数的参数在内存(栈)的存储情形。
#include "uart.h"int printf_test(const char fmt, ...){ return 0;}int main(int argc, char argv){ printf_test("abc", 1, 2, 3, 4, 5, 6); return 0;}
从上面的代码可知,我们往printf_test函数通报了7个参数。接下通过 arm-linux-guneabihf-gcc 编译器编译以上代码,并反汇编,个中以上程序对应的反汇编代码如下:(注:只截取了一部分)
uart.elf: file format elf32-littlearmDisassembly of section .text:87800000 <_start>:87800000:e3a0d482 movsp, #-2113929216; 0x82000000 /把栈顶地址设置为0x82000000/87800004:ea000008 b8780002c <main> /跳转到main函数实行/87800008 <printf_test>:87800008:e92d000f push{r0, r1, r2, r3} /把r3、r2、r1、r0依次入栈/8780000c:e52db004 push{fp}; (str fp, [sp, #-4]!) /fp 入栈,存的是main函数的fp/87800010:e28db000 addfp, sp, #0 /更新fp/87800014:e3a03000 movr3, #0 87800018:e1a00003 movr0, r3 /r0作为printf_test的返回值/8780001c:e24bd000 subsp, fp, #0 87800020:e49db004 pop{fp}; (ldr fp, [sp], #4) /main函数的fp出栈/87800024:e28dd010 addsp, sp, #16 /sp回滚,肃清printf_test所用的栈空间/87800028:e12fff1e bxlr /返回/8780002c <main>:8780002c:e92d4800 push{fp, lr} /把 lr、fp 寄存器依次压栈,fp寄存器便是R11寄存器被称为栈帧寄存器,与sp一同构成函数所用的栈区间/87800030:e28db004 addfp, sp, #4 /更新fp寄存器,即此时的fp所指的地方便是main函数所用栈的起始地址/87800034:e24dd018 subsp, sp, #24 /开辟24字节的栈空间,4字节对齐/87800038:e50b0008 strr0, [fp, #-8] /在fp-8的地方(即紧随着前fp压栈的地方)存入r0/8780003c:e50b100c strr1, [fp, #-12] /接着存入r1/87800040:e3a03006 movr3, #687800044:e58d3008 strr3, [sp, #8] /存入printf_test的末了一个参数/87800048:e3a03005 movr3, #58780004c:e58d3004 strr3, [sp, #4] /存入printf_test的倒数第二个参数/87800050:e3a03004 movr3, #487800054:e58d3000 strr3, [sp] /存入printf_test的倒数第三个参数/87800058:e3a03003 movr3, #3 /把printf_test的倒数第四个参数存入r3寄存器/8780005c:e3a02002 movr2, #2 /把printf_test的倒数第五个参数存入r2寄存器/87800060:e3a01001 movr1, #1 /把printf_test的倒数第六个参数存入r1寄存器/87800064:e3000080 movwr0, #128; 0x8087800068:e3480780 movtr0, #34688; 0x8780 /把printf_test的第一个参数存入R0,这里存入R0的是第一个参数的地址指针,实行movw、movt这两个指令后,r0 = 0x87800080,即第一天参数的内容存放在0x87800080这个地址,在该地址存放的是0x00636261,即abc的ascii值/8780006c:ebffffe5 bl87800008 <printf_test> /调用printf_test函数/87800070:e3a03000 movr3, #087800074:e1a00003 movr0, r387800078:e24bd004 subsp, fp, #48780007c:e8bd8800 pop{fp, pc}
注:① 上面汇编代码的链接地址是 0x87800000,代码从 _start 开始实行;② 栈顶的地址设置为0x82000000,设置栈顶地址后,跳转到C程序main函数实行。
其余上面的汇编代码涉及的两条轻微陌生的汇编指令:
movw : 把 16 位立即数放到寄存器的底16位,高16位清0;
movt : 把 16 位立即数放到寄存器的高16位,低 16位不影响。
经由对汇编代码的剖析,代码从_start开始实行到main函数调用printf_test后返回的栈空间利用分布图如下:
栈空间利用分布图
从上图可知,传入printf_test函数的7个参数被依次从右到左存放到了内存连续的占空间,因此只要得到第一个参数的地址,便可根据连续的内存地址访问到其他剩余的参数。
注:这个7个参数入栈分成了两部分入栈:① 在main函数中把最右边的3个参数(4、5、6)存到栈中,把前面的三个参数分别存放到R0~R3;② printf_test函数然后依次把R3、R2、R1、R0入栈。
编写代码测试,添补printf_test的代码,把传入的参数打印出来。(实验平台为正点原子IMX6ULL开拓板,通过UART1串口输入打印信息)
#include "uart.h"int printf_test(const char fmt, ...){ char p = (char )&fmt; putstr("arg1:");putstr((char )fmt);putstr("\r\n"); p = p + sizeof(char ); /sizeof(char ) = 4 由于 imx6ull是32bit的CPU,所有存char数据类型的地址是32bit,即4字节/ putstr("arg2:");putnum(p, 10);putstr("\r\n"); /putnum的第一个参数是要打印的数字,第一个参数是进制,例如putnum(5,10)表示以10进制的办法打印5/ p = p + sizeof(char ); putstr("arg3:");putnum(p, 10);putstr("\r\n"); p = p + sizeof(char ); putstr("arg4:");putnum(p, 10);putstr("\r\n"); p = p + sizeof(char ); putstr("arg5:");putnum(p, 10);putstr("\r\n"); p = p + sizeof(char ); putstr("arg6:");putnum(p, 10);putstr("\r\n"); p = p + sizeof(char ); putstr("arg7:");putnum(p, 10);putstr("\r\n"); return 0;}int main(void){ uart_init(); printf_test("abc", 1, 2, 3, 4, 5, 6); while(1); return 0;}
把编译好的程序拿到 imx6ull 开拓板运行,串口输入的结果如下:
由此可见,这七个参数的栈空间是连续的,我们知道第一个参数的地址,便可根据连续的内存地址访问到其他剩余的参数。
2. 改进printf_test打印程序在VC6.0 头文件stdarg.h中有如下代码:
typedef char va_list; /重命名char 为va_list /#define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1)) #define va_start(ap,v) (ap = (va_list)&v + _INTSIZEOF(v)) #define va_arg(ap,t) ((t )((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)))#define va_end(ap) ( ap = (va_list)0 )
(1) _INTSIZEOF(n) : 用于获取个中一个变参类型占用的空间长度,4字节对齐;(2) va_start(ap,v) :令 ap 指向第一个变参的地址;(3) va_arg(ap,t) :取出一个变参的内容,同时把指针指向下一个变参的地址;对付表达式
((t )((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)))
上面表达式的运算顺序为:
① 先运算ap += _INTSIZEOF(t),即ap指向了下一个可变参数的首地址,改变了ap的值;
② 然后打算 [ ap=ap+_INTSIZEOF(t)] - _INTSIZEOF(t),还原当前变量的地址,此时ap的值没有发生改变(即此时ap的值为第①步运算的值,也便是下一个可变参数的地址)。
③ (t)把当前变量的地址逼迫转换为t类型的指针,然后 (t)取该地址的内容;
④ 末了就实现了取出一个变参的内容,同时把指针指向下一个变参的地址。
(4) va_end(ap):将指针指向 NULL, 防止野指针。
有了上面stdarg.h代码,我们可以把上面的printf_test函数的代码改为:
#include "uart.h"typedef char va_list; #define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1)) #define va_start(ap,v) (ap = (va_list)&v + _INTSIZEOF(v)) #define va_arg(ap,t) ((t )((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)))#define va_end(ap) ( ap = (va_list)0 )int printf_test(const char fmt, ...){ va_list ap; va_start(ap, fmt); putstr("arg1:");putstr((char )fmt);putstr("\r\n"); putstr("arg2:");putnum(va_arg(ap,int), 10);putstr("\r\n"); putstr("arg3:");putnum(va_arg(ap,int), 10);putstr("\r\n"); putstr("arg4:");putnum(va_arg(ap,int), 10);putstr("\r\n"); putstr("arg5:");putnum(va_arg(ap,int), 10);putstr("\r\n"); putstr("arg6:");putnum(va_arg(ap,int), 10);putstr("\r\n"); putstr("arg7:");putnum(va_arg(ap,int), 10);putstr("\r\n"); return 0;}int main(void){ uart_init(); printf_test("abc", 1, 2, 3, 4, 5, 6); while(1); return 0;}
代码运行结果与前面相同,如下图所示:
3. 根据可变参数函数实现的事理,编写用于裸机调试的printf函数
(1) 基于正点原子imx6ull开拓板 uart1 的uart.c代码如下:
#include "uart.h"#include "imx6ul.h"void uart_init(void){ /1.使能UART1时钟/ CCM->CCGR5 |= CCM_CCGR5_CG12(0x3); /2.设置引脚复用为UART1功能/ IOMUXC_SetPinMux(IOMUXC_UART1_TX_DATA_UART1_TX, 0); IOMUXC_SetPinMux(IOMUXC_UART1_RX_DATA_UART1_RX, 0); /3.设置硬件参数,设置为默认值0x10B0/ IOMUXC_SetPinConfig(IOMUXC_UART1_TX_DATA_UART1_TX, 0x10B0); IOMUXC_SetPinConfig(IOMUXC_UART1_RX_DATA_UART1_RX, 0x10B0); /4.关闭当前串口/ UART1->UCR1 |= (1 << 0); /5.设置UART1传输格式: UART1中的UCR2寄存器的关键bit如下: [14]: 1:忽略RTS引脚 [8]: 0:关闭奇偶校验 默认为0; [6]: 0:停滞位1位 默认为0; [5]: 1:数据长度8位 [2]: 1:发送数据使能 [1]: 1:接管数据使能 / UART1->UCR2 |= (1 << 14) | (1 << 5) | (1 << 2) | (1 << 1); /6.设置串口MUXED模型,bit2必须设置为1/ UART1->UCR3 |= (1 << 2); /7.设置波特率 根据芯片手册得知波特率打算公式: Baud Rate = Ref Freq / (16 (UBMR + 1)/(UBIR+1)) 当我们须要设置 115200的波特率 UART1_UFCR [9:7]=101,表示不分频,得到当前UART参考频率Ref Freq :80M , 带入公式:115200 = 80000000 /(16(UBMR + 1)/(UBIR+1)) 选取一组知足上式的参数:UBMR、UBIR即可 UART1_UBIR = 71 UART1_UBMR = 3124 / UART1->UFCR = 5 << 7; / Uart的时钟clk:80MHz / UART1->UBIR = 71; UART1->UBMR = 3124; /8.使能串口/ UART1->UCR1 |= (1 << 0);}void putchar(unsigned char c){ while (!((UART1->USR2) & (1 << 3))); /等等上一个字符发送完毕/ UART1->UTXD = c & 0xff; }unsigned char getchar(void){ while(!((UART1-> USR2) & (1 << 0))); return (unsigned char)UART1->URXD;}void puts(char s){ while(s) { putchar((unsigned char)s); s++; }}char num_tab[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9','a','b','c','d','e','f'};/ 功能:按照base的进制值打印num对应的进制数 参数: num: 输入打印的数值 base:进制值 flag: 1: 把num 转换为有符号数打印; 0:num为无符号数打印 /void putnum(long num, int base, int flag){ unsigned long m; char buf[30]; char s = buf + sizeof(buf); --s = '\0'; if(num < 0 && flag == 1) m = -num; else m = (unsigned long)num; do{ --s = num_tab[m % base]; m /= base; }while(m != 0); if(num < 0 && flag == 1) --s = '-'; puts(s);}int raise(void){ return 0;}
注:① putnum函数利用到除法运算和求模运算,须要供应除法库,否则编译时会产生如下缺点:
一样平常的交叉编译工具链都有基本的数据运算,它位于libgcc.a,因此,为了支持除法运算,我们修正Makefile要把libgcc.a 链接到程序里,链接程序的Makefile命令修正如下所示:
built-in.o : $(curdir_objs) $(subdir_objs)$(LD) -r -o $@ $^ -lgcc -L/tools/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/lib/gcc/arm-linux-gnueabihf/4.9.4/
注:链接指令中,每个“-L”表示库在哪里,即它的目录;“-l” 表示哪个库,即库的名称, -lgcc 表示会链接“libgcc.a”库。本人am-linux-gnueabihf-gcc编译器的libgcc.a 的路径是 /tools/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/lib/gcc/arm-linux-gnueabihf/4.9.4/。
② 添加完libgcc.a库后,重新编译会产生以下缺点:
arm-linux-gnueabihf-ld -Timx6ull.lds -o uart.elf built-in.obuilt-in.o: In function `__aeabi_idiv0':/home/tcwg-buildslave/workspace/tcwg-make-release/label/docker-trusty-amd64-tcwg-build/target/arm-linux-gnueabihf/snapshots/gcc-linaro-4.9-2017.01/libgcc/config/arm/lib1funcs.S:1331: undefined reference to `raise'make: [Makefile:39: all] Error 1
这个缺点的办理方法:添加raise函数。
int raise(void){ return 0;}
(2) 实现printf函数的printf.c文件代码如下:
#include "uart.h"typedef char va_list; #define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1)) #define va_start(ap,v) (ap = (va_list)&v + _INTSIZEOF(v)) #define va_arg(ap,t) ((t )((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)))#define va_end(ap) ( ap = (va_list)-1 )static int vprintf(const char fmt, va_list ap){ for(; fmt != '\0'; fmt++) { if(fmt != '%'){ putchar(fmt); continue; /终止本次for循环,即本次循环实行到这里不再往下实行,开始下一次for循环/ } fmt++; switch(fmt){ case 'd': putnum(va_arg(ap, int), 10, 1);break; case 'o': putnum(va_arg(ap, unsigned int), 8, 0); break; case 'u': putnum(va_arg(ap, unsigned int), 10, 0); break; case 'x': putnum(va_arg(ap, unsigned int), 16, 0); break; case 'c': putchar(va_arg(ap, int)); break; case 's': puts(va_arg(ap,char )); break; default: putchar(fmt); break; } } return 0;}int printf(const char fmt, ...){ va_list ap; va_start(ap, fmt); vprintf(fmt, ap); va_end(ap); return 0;}
注:由于printf函数的名称与C措辞库的printf冲突(前面uart.c的putchar、puts同理),编译时会产生以下警告:
办理这类警告的方法:在arm-linux-gnueabihf-gcc添加 -fno-builtin 编译选项。
(3) 编写测试程序:
#include "uart.h"int main(void){ uart_init(); printf("printf test\r\n"); printf("test char:%c, %c\r\n",'a', 'B'); printf("test decimal num:%d\r\n", 123456); printf("test decimal num:%d\r\n", -123456); printf("test hex num:0x%x\r\n", 0x55aa55aa); printf("test oct num:0%o\r\n",012); /C 措辞八进制以0开头,把稳是数字 0,不是字母 o/ printf("test unsigned num:%u\r\n", -1); printf("test string: %s\r\n", "Hello world!"); while(1); return 0;}
编译后,打印的结果如下图所示:
注:(1) 有符号数逼迫转为无符号数:① 有符号数为正数,逼迫转换为无符号数,转换前后不变;② 有符号数为负数,逼迫转换为无符号数是有符号数的补码(负数补码:符号位(即最高位)除外,剩余位取反,加1;正数补码:与原码相同)。
(2) 无符号数逼迫转换为有符号数:① 符号位(即最高位)为0,有符号数与无符号数一样;② 符号位(即最高位)为1,有符号数是无符号数的补码。