ARMC语⾔可变参数函数实现原理
注:本⽂参考韦东⼭新⼀期裸机视频《从零实现⽤于裸机调试的printf函数》,只⽤于学习记录。
1. ARM C语⾔可变参数实现原理
在我们写C语⾔程序时,经常使⽤到 printf 函数打印,⽽ printf 函数就是⼀个可变参数函数,它的函数原型如下:(在ubuntu终端输⼊man 3 printf 命令即可查看)
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,...)
{
return0;
}
int main(int argc,char**argv)
{
printf_test("abc",1,2,3,4,5,6);
return0;
}
从上⾯的代可知,我们往printf_test函数传递了7个参数。接下通过 arm-linux-guneabihf-gcc 编译器编译以上代码,并反汇编,其中以上程序对应的反汇编代码如下:(注:只截取了⼀部分)
uart.elf:    file format elf32-littlearm
Disassembly of section .text:
87800000<_start>:
87800000: e3a0d482  mov sp, #-2113929216;0x82000000/*把栈顶地址设置为0x82000000*/
87800004: ea000008  b 8780002c <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  add fp, sp, #0/*更新fp*/
87800014: e3a03000  mov r3, #0
87800018: e1a00003  mov r0, r3      /*r0作为printf_test的返回值*/
8780001c: e24bd000  sub sp, fp, #0
87800020: e49db004  pop {fp};(ldr fp,[sp], #4)/*main函数的fp出栈*/
87800024: e28dd010  add sp, sp, #16/*sp回滚,消除printf_test所⽤的栈空间*/
87800028: e12fff1e  bx lr        /*返回*/
8780002c <main>:
8780002c: e92d4800  push {fp, lr}/*把 lr、fp 寄存器依次压栈,fp寄存器就是R11寄存器被称为栈帧寄
存器,与sp⼀同构成函数所⽤的栈区间*/ 87800030: e28db004  add fp, sp, #4/*更新fp寄存器,即此时的fp所指的地⽅就是main函数所⽤栈的起始地址*/
87800034: e24dd018  sub sp, sp, #24/*开辟24字节的栈空间,4字节对齐*/
87800038: e50b0008  str r0,[fp, #-8]/*在fp-8的地⽅(即紧跟着前fp压栈的地⽅)存⼊r0*/
8780003c: e50b100c  str r1,[fp, #-12]/*接着存⼊r1*/
87800040: e3a03006  mov r3, #6
87800044: e58d3008  str r3,[sp, #8]/*存⼊printf_test的最后⼀个参数*/
87800048: e3a03005  mov r3, #5
8780004c: e58d3004  str r3,[sp, #4]/*存⼊printf_test的倒数第⼆个参数*/
87800050: e3a03004  mov r3, #4
87800054: e58d3000  str r3,[sp]/*存⼊printf_test的倒数第三个参数*/
87800058: e3a03003  mov r3, #3/*把printf_test的倒数第四个参数存⼊r3寄存器*/
8780005c: e3a02002  mov r2, #2/*把printf_test的倒数第五个参数存⼊r2寄存器*/
87800060: e3a01001  mov r1, #1/*把printf_test的倒数第六个参数存⼊r1寄存器*/
87800064: e3000080  movw r0, #128;0x80
87800068: e3480780  movt r0, #34688;0x8780/*把printf_test的第⼀个参数存⼊R0,这⾥存⼊R0的是第⼀个参数的地址指针,执⾏movw、movt这两个指令后,r0 = 0x87800080,即第⼀天参数的内容存放在0x87800080这个地址,在该地址存放的是0x00636261,即abc的ascii值*/
8780006c: ebffffe5  bl 87800008<printf_test>/*调⽤printf_test函数*/
87800070: e3a03000  mov r3, #0
87800074: e1a00003  mov r0, r3
87800078: e24bd004  sub sp, fp, #4
8780007c: e8bd8800  pop {fp, pc}
注:① 上⾯汇编代码的链接地址是 0x87800000,代码从 _start 开始执⾏;② 栈顶的地址设置为0x82
000000,设置栈顶地址后,跳转到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");
return0;
}
int main(void)
{
uart_init();
printf_test("abc",1,2,3,4,5,6);
while(1);
return0;
}
把编译好的程序拿到 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");
sizeof 指针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");
return0;
}
int main(void)
{
uart_init();
printf_test("abc",1,2,3,4,5,6);
while(1);
return0;
}
代码运⾏结果于前⾯相同,如下图所⽰:
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为⽆符号数打印

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。