An implementation of variadic macro in C language

  1. It's been a long time since I wrote, and it's a bit of a mess.
  2. The following mainly analyzes an implementation of variable parameter macros.
  3. Because the C language standard library is platform (processor) dependent, this procedure cannot guarantee that all processors will be available.
  4. I have implemented a similar printf function on a bare metal ARM processor, which is suitable for use on the ARM platform.

  5. The following code is from the stdarg.h header file of the linux kernel
  6. typedefchar *va_list;   
  7.   
  8. ​#define _AUPBND (sizeof (acpi_native_int) - 1)  
  9.   
  10. ​#define _ADNBND (sizeof (acpi_native_int) - 1)  
  11.   
  12. ​#define _bnd(X, bnd) (((sizeof (X)) + (bnd)) & (~(bnd)))  
  13.   
  14. ​#define va_arg(ap, T) (*((T*) (((ap) += (_bnd (T, _AUPBND))) - (_bnd (T,_ADNBND))))  
  15.   
  16. ​#define va_end(ap) (void) 0  
  17.   
  18. ​#define va_start(ap, A) (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND))))

The following mainly analyzes the above lines of code and implements it by yourself.

  1. typedefchar *va_list;   

This line is mainly to define a pointer of type char. (Why is char not other types?)

Answer: The main reason is that the pointer of char type is added, and each increase is based on 1 byte. Each time the type of int* is added by 1 is actually an increase of 4 bytes. So pointers of type char are more flexible to operate.

  1. ​#define _AUPBND (sizeof (acpi_native_int) - 1)  
  2. ​#define _ADNBND (sizeof (acpi_native_int) - 1)  

These two lines are platform dependent

It is mainly implemented in the following two ways

typedef s32  acpi_native_int If this macro definition is used, it indicates that the int type is represented by 32 bits, and it also indicates that the current memory is 4-byte aligned

If typedef s64 acpi_native_int         uses this macro definition, it indicates that the int type is represented by 64 bits, and it also indicates that the current memory is 8-byte aligned

This article explains it with 4-byte alignment.

  1. ​#define _AUPBND (sizeof (acpi_native_int) - 1)  
  2. ​#define _ADNBND (sizeof (acpi_native_int) - 1)  

After entering the above formula, you can get the representation of the macro

  1. ​#define _AUPBND 3
  2. ​#define _ADNBND 3

  1. ​#define _bnd(X, bnd) (((sizeof (X)) + (bnd)) & (~(bnd)))  
Continue to bring the above formula to get

  1. ​#define _bnd(X, bnd) (((sizeof (X)) + 3) & (~(3)))  

3 is represented as 11b in binary, that is, the lower two bits are 1

sizeof  (X) usually gets the number of bytes of a type, usually 1, 4, 8....

Bringing 1, 4, 8 into the above formula can find that the result is

#define _bnd(char,3)    ==>  (1+3)&(~3)  ==> 4

#define _bnd(int,3)     ==>  (4+3)&(~3)  ==> 4

#define _bnd(double,3)  ==>  (4+3)&(~3)  ==> 8

........

It can be found that it is an integer multiple of 4, and at the same time, whether the incoming type is 4 (sizeof(int)) or not, after #defeine _bnd, it is 4-byte aligned upwards

Its function is also to align the transfer in the process of parameter passing (such as char type, if the parameter is passed into the stack, it is 4-byte aligned and pushed onto the stack)

Next, let's talk about the function passing parameters in C language. When the number of parameters is less than 4 (maybe others, related to the compiler), it will be passed through registers such as r0, r1, r2, r3

After entering the function, these registers still need to be pushed onto the stack (because registers are used in the function).

The stacking sequence is opposite to the parameter passing sequence. For example, the following xxx function pushes parameter c first, then parameter b, and finally pushes parameter a.

void xxx(int a,int b,int c)

Because the ARM platform defaults to full subtraction, the parameter c has a high address, and the parameter a has a low address.

When the number of parameters exceeds 4 (it may also be others), other parameters are also pushed from the back to the front, and the first 4 are still passed in the register and pushed to the stack.

Suppose there are five parameters, void xxx(int ​​a, int b, int c, int d, int e), the final arrangement in memory is as follows


If you know the address of a and the type of each parameter, you can get the value of each parameter through the pointer.

Next, introduce variable parameter functions, take printf as an example

下面是printf函数的原型

int printf(const char *format, ...);

在printf函数中,知道了第一个参数的地址(通常第一个参数都为指针类型),则可以通过%d,%s,%x,%f之类得到参数类型,进而通过指针运算找到对应参数。

下面这个printf的使用为例子来分析

printf ("   %s .%d..%lf ", p_char,int_i,double_d);

下面根据上面的分析,画出,四个参数在内存中的分布图。


接下来主要分析可变参数中用到的宏(即如何通过指针运算找到真确地址)

  1. ​#define va_start(ap, A) (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND))))

va_start(ap, A)有两个参数,一个是函数传入的第一个参数(比如printf中的format),必须有这个参数,才能进行后面的数据索引

ap为用户自定义的一个参数通常用系统给定的va_list来定义   表示为va_list ap    ==>   char * ap

该宏为初始化ap,同时得到下一个参数的地址。

带入p参数

  1. ​#define va_start(ap, format) (void) ((ap) = (((char *) &(format)) + (_bnd (format,3))))

因为format为一个指针类型,32位平台为4个字节,所以后半部分的表达式经过对齐运算后_bnd (format,3)为4

即    ap = (void)((char*)&(format) + 4) 恰好format指针为四个字节,接下来ap指向p_char的字符指针类型。

而取出字符串的方法也很简单,使用下面这个宏即可实现。

  1. ​#define va_arg(ap, T) (*((T*) (((ap) += (_bnd (T, _AUPBND))) - (_bnd (T,_ADNBND)))) 

这个宏有两个参数,一个是字符指针类型ap,另一个为T类型,即要在ap这个起始地址取出的数据的类型。

因为第一个参数中的%s以及提示第二个参数为字符指针类型。

则最简单的方法就是  把char*类型的指针ap转换为 T类型的指针ap,然后进行引用操作

转换为T类型的指针可以这样操作(T*)ap 

然后取出这个值就很简单了*((T*)ap 

va_arg这个宏还要满足取值的值返回,同时ap指向下一个参数的地址,所以一条语句满足两个结果就稍微麻烦一些。

最简单的方法是使用逗号表达式,一条语句满足实现两种功能,并返回逗号前面的值

比下面这种实现:

  1. ​#define va_arg(ap, T) (*((T*) (ap)), ap += (_bnd (T, _AUPBND)))

逗号前面的是取值,逗号后面的是让ap指向下一个参数的地址(其实就是va_start

而内核使用了一种比较花哨的写法

先让ap 加上下一个参数的偏移量,然后又减去这个偏移量。然后得到对应值。

  1. ​#define va_arg(ap, T) (*((T*) (((ap) += (_bnd (T, _AUPBND))) - (_bnd (T,_ADNBND)))) 

标红的为让ap指向下一个参数的地址。但后面减去偏移量的值不用ap保存接收,而是直接通过类型转换,得到值。

起始和用逗号表达式的效果一致。但唯一对使用者来说看起来不是那么容易理解,但可能对库本身的实现的那类人来说,应该和逗号表达式的看起来 难度是一样的。



Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325905474&siteId=291194637