Background introduction

Recently I was reading a book called “Self-cultivation in Embedded C language”, which helped me a lot. It is a good book. In Chapter 6, the GNU C Compiler Extended Syntax Tutorial, the book gives some examples of variable-parameter functions:

//1. Initial experience of variable parameter function
#include<stdio.h>
void print_num(int count,...)
{
	int *args;
	args = &count + 1;
	for(int i = 0; i < count; i++) {printf("*args:%d\n",*args); args++; }}int main(void)
{
	print_num(5.1.2.3.4.5);
	return 0;
}
Copy the code

The above code is easy to understand: we define a variable function print_num, which takes the address of the first argument and assigns it to a pointer, then moves the pointer back, takes the remaining arguments and prints them out. Print_num = print_num; print_num = print_num;

*args:1
*args:2
*args:3
*args:4
*args:5
Copy the code

But the results were surprising:

The values printed out and the values passed in are completely different, even irregular.

Problem analysis

In the above code, we take the address of the first argument and move the pointer backwards to get the next argument, so the problem is most likely in two places:

  1. The pointer is not moving correctly
  2. The address arrangement of the parameter may not be contiguous

Let’s take it one at a time, assuming for a moment that the parameter addresses are contiguously spaced by the same distance. So we can focus on how the pointer moves. Pointer movement is controlled by the line “args++”. I modified the code in the book:

#include<stdio.h>
void print_num(int count,...)
{
	int *args;
	args = &count;
	for(int i = 0; i <= count; i++) {printf("addr:%p\n",args);
		printf("*args:%d\n",*args); args++; }}int main(void)
{
	print_num(5.1.2.3.4.5);
	return 0;
}
Copy the code

Mainly increased the print of the address of each parameter, the running result is as follows:

The author found that “args++” moves back 4 bytes at a time, because the basic unit for moving an int* pointer is 4 (sizeof(int)). Similarly, for a “char*” pointer move, the unit is 1(sizeof(char)).

Pointer to the size

If the size of an int* pointer is equal to 4, then the move operation is ok. But is the size of an int* pointer really 4?

I use code to test:

#include<stdio.h>

int main(a)
{
	char*	charPoint;
	int*	intPoint;
	double*	doublePoint;

	struct st{
		int first;
	};

	struct st *structPoint;

	printf("sizeof(char*):%ld\n".sizeof(charPoint));
	printf("sizeof(int*):%ld\n".sizeof(intPoint));
	printf("sizeof(double*):%ld\n".sizeof(doublePoint);
	printf("sizeof(struct*):%ld\n".sizeof(structPoint));
	return 0;
}
Copy the code

Running result:

As you can see, not only is the “int*” pointer 8 bytes, but the “char*”, “double*”, and structure Pointers are 8 bytes too. This is because my computer is running on a 64-bit system. I changed it to “args += 2”, which is correct in dev c++ IDE, but not in ubuntu GCC.

Parameter position arrangement

We solved the first pointer move step problem, but we still didn’t get the correct answer. I suspect that the parameter addresses are likely to be discontinuous. How to view the parameter address information of a function? There are many methods, the author chose a more quick way – look at the assembly code.

Enter in the Ubuntu terminal box

GCC -s [source]

You get an assembly code file with an “. S “suffix.

Let’s look at the argument passing part of main versus print_num:

In the main function, the arguments are put into different registers, and in the print_num function, the arguments are taken out of the registers and put into the print_num function stack. A closer look at where each argument ends up on the stack shows that the address of the first argument is 28 bytes off the address of the second argument, and the address of the following arguments is 8 bytes off. This explains why the previous code did not turn out right.

To solve the problem

So just add the offset 28 (“char*”) to the address of the first argument.

The results are as expected:

Why the first and second arguments are 28 bytes apart is not clear, so you need to look at the compiler in GCC for a blind guess.

Additional tests

In the past, for ordinary functions with a fixed number of parameters, it is treated as follows: the first few parameters are put into the register, if the number exceeds, then pushed into the function stack. Print_num = print_num = print_num = print_num

The assembly code is as follows:

This shows that the rules of passing parameters for variable-parameter functions are the same as for ordinary functions.

conclusion

When reading a book, I like to read while typing code, this time according to the book of the code to run the result is wrong, there are some of the above inquiry process. If I didn’t do it, I would probably be confused when I encounter similar problems in the future. So hands-on practice is necessary.

In addition, everything in the book is not always correct, and its correctness needs to have certain premises to ensure. For example, if I’m using a 32-bit system and the compiler is handling variator functions with arguments pressed continuously, then the code in the book is perfectly correct. We don’t need to be afraid of these pits. What we need to do is to find the preconditions, to find the essence of the problem, and finally solve the problem.

The resources

Embedded C Self-cultivation: From Chips, Compilers to Operating Systems

I also have a personal blog site: Lularible’s personal blog, which you are welcome to visit.