added short chapter on variadic functions

This commit is contained in:
Perry Kivolowitz 2023-01-24 19:40:36 -06:00
parent 69c2a4c465
commit f3f3f84b3c
5 changed files with 153 additions and 0 deletions

View file

@ -303,6 +303,7 @@ What would a book about assembly language be without bit bashing?
| --- | [Under the hood: System Calls](./more/system_calls/README.md) | [Link](./more/system_calls/README.pdf) |
| --- | [Apple Silicon](./more/apple_silicon/README.md) | [Link](./more/apple_silicon/README.pdf) |
| --- | [Apple / Linux Convergence](./macros) | [Link](./macros/README.pdf) |
| --- | [Variadic Functions](./more/varargs/README.md) | [Link](./more/varargs/README.pdf) |
## Macro Suite

Binary file not shown.

69
more/varargs/README.md Normal file
View file

@ -0,0 +1,69 @@
# varargs
Mentioned elsewhere, how Apple and Linux handle variadic functions are
quite different. In this chapter we will explore how variadic functions
work by looking at an example in C++. Then, we'll review the differences
between Apple and Linux.
## How Variadic Functions Determine Number of Parameters
The TL;DR is that something among the *initial* arguments tells the
function how many other arguments to expect. Commonly, it is the very
first argument that contains this information but it is possible to
encode this information in a parameter other than the first as long as
no optional parameter precedes it.
For example, take our friend and yours: `printf`. The first argument is
a template or formatting string that may contain zero or more instances
of placeholders. For example:
| Sample | Expected Arguments |
| ------ | ------------------ |
| `printf("Foo");` | None |
| `printf("%%d");` | None - notice the double % |
| `printf("%d %ld");` | An int and a long |
| `printf("%p %f");` | A pointer and a double |
The `printf` function figures out how many arguments to expect and what
types to expect from the first parameter.
## Writing Variadic Functions in C or C++
You'll need to include `cstdarg` in C++ or `stdarg.h` in C++ or C.
Then, you'll use the **macros** `va_start()` and `va_end()` plus a
variable of type `va_list`.
* `va_start()` is used to initialize the `va_list` variable with the
address of the last required argument. This enables the variadic support
functions to find the variadic parameters on the stack or in registers.
* `va_arg()`, another macro fetches the next variadic argument. The type
of the argument is given as a parameter to `va_arg()`.
* `va_end()` is used to clean up the internal data structure allocated
for you, behind your back.
Take a look at `Simple()` in [this source code](./main.cpp).
For a more sophisticated example, look at `LessSimple()` in the same
file for something closer to `printf()`.
The above referenced source code contains ample commenting to explain
what it is doing.
## Differences Between Apple and Linux
### Linux
Linux uses the first 8 scratch registers as normal. If you need to
specify more than 8 arguments, the ninth and beyond go on the stack. It
is up to you to determine how many arguments to expect (which you must
do anyway) to determine where to find them.
### Apple
Apple puts the first argument in `x0` as usual but all remaining
arguments go on the stack in right to left order. There are some
other restrictions - it is best to refer to [this](https://developer.apple.com/documentation/xcode/writing-arm64-code-for-apple-platforms
) cryptically written document.

BIN
more/varargs/README.pdf Normal file

Binary file not shown.

83
more/varargs/main.cpp Normal file
View file

@ -0,0 +1,83 @@
#include <iostream>
#include <iomanip>
#include <cinttypes>
#include <string>
#include <cstdarg>
#include <math.h>
using namespace std;
/* va_start() and va_end() are MACROS not functions themselves. The implementation
of these are architecture dependent. A common implementation is that va_start()
takes the address of the last non-variadic variable which identifies where on
the stack the following arguments can be found. The format string (or whatever)
determines how to advance the pointer. Some architectures are quite complicated.
For example, here is a comment from the implementation of va_start:
//
* In GNU the stack is not necessarily arranged very neatly in order to
* pack shorts and such into a smaller argument list. Fortunately a
* neatly arranged version is available through the use of __builtin_next_arg.
//
*/
/* Simple() - a first example of a variadic function in which all
parameters are assumed to be pointers to char.
*/
void Simple(uint32_t count, ...) {
int digits_needed = (int) ceil(log10((double) count)) + 1;
va_list args;
va_start(args, count);
for (uint32_t index = 0; index < count; index++) {
cout << "Index: [" << setw(digits_needed) << index << "] " << va_arg(args, char *) << endl;
}
va_end(args);
}
/* LessSimple() is a bit more like printf in that multiple data types are
supported, passed in in a format string. This function is an important
lesson in the types of data types that are supported by varargs. Note
that a float is passed as a double and char and short are passed by as
ints. The patterns is that if a data type is automatically promotable,
you must handle it as the highest level of promotion.
For example, a char is an int so it must be handled like an int. Floats
are automatically promotable to double, so they must be supported as
doubles. An int is not automatically promoted to long, so longs can
be dealt with directly.
*/
void LessSimple(string fmt, ...) {
va_list args;
va_start(args, fmt);
for (uint32_t index = 0; index < fmt.size(); index++) {
switch (fmt.at(index)) {
case 'i':
cout << "i: " << va_arg(args, int64_t) << endl;
break;
case 'f':
cout << "f: " << (float) va_arg(args, double) << endl;
break;
case 'd':
cout << "d: " << va_arg(args, double) << endl;
break;
case 'c':
cout << "c: " << (int8_t) va_arg(args, int32_t) << endl;
break;
case 's':
cout << "s: " << (int16_t) va_arg(args, int32_t) << endl;
break;
default:
break;
}
}
va_end(args);
}
int main() {
Simple(5, "Foo", "is", "a", "Variadic", "Function");
Simple(13, "Foo", "is", "a", "Variadic", "Function", "which means", "it", "can", "take", "any", "number", "of", "args");
LessSimple(string("ifdcs"), 99L, 4.4f, 4.4, 'c', (int16_t) 30000);
return 0;
}