Closed agagniere closed 1 year ago
Apple documentation on what is specific to their architecture : writing arm64 code for apple platforms In particular (emphasis mine) :
Update code that passes arguments to variadic functions
For functions that contain a variable number of parameters, Apple initializes the relevant registers (Stage A) and determines how to pad or extend arguments (Stage B) as usual. When it’s time to assign arguments to registers and stack slots, Apple platforms use the following rules for each variadic argument:
- Round up the Next SIMD and Floating-point Register Number (NSRN) to the next multiple of 8 bytes.
- Assign the variadic argument to the appropriate number of 8-byte stack slots.
Because of these changes, _*the type va_list is an alias for `char`, and not for the struct type in the generic procedure call standard**_. The type also isn’t in the std namespace when compiling C++ code.
Note
The C language requires the promotion of arguments smaller than int before a call. Beyond that, the Apple platforms ABI doesn’t add unused bytes to the stack.
In a way, this va_list
implementation isn't specific to Apple Silicon as it is how it used to be on i386 (32bit x86).
What is new here is that they specify a different calling convention for variadic functions that for other functions. (And that is why C functions interface in other languages had issues on Apple Silicon, e.g. Java Native Runtime FFI, CPython ctypes)
To compare, on amd64 va_list
is an address to a complex structure : amd64 and va_arg
typedef struct { unsigned int gp_offset; unsigned int fp_offset; void *overflow_arg_area; void *reg_save_area; } va_list[1];
So. the difference is that va_list
used to be a pointer to a struct, and va_arg
could modify the struct, without needing modify its address. Here va_list
is a pointer to the stack, and it is the pointer itself that va_arg
modifies.
So va_list
must be passed by reference, as simple as that...
...excepts that is then fails on amd64
I initially took it by value for vprintf
and consorts, as it is the standard : C standard for vprintf
int vprintf( const char *restrict format, va_list vlist );
Interesting reads :
foo.c
#include <stdio.h>
#include "libft.h"
void foo(char c, short h, int i, long L, short s, char b)
{
printf("%2ti : %hhi\n", (void*)&c - (void*)&c, c);
printf("%2ti : %hi\n", (void*)&c - (void*)&h, h);
printf("%2ti : %i\n", (void*)&c - (void*)&i, i);
printf("%2ti : %li\n", (void*)&c - (void*)&L, L);
printf("%2ti : %hi\n", (void*)&c - (void*)&s, s);
printf("%2ti : %hhi\n", (void*)&c - (void*)&b, b);
ft_print_memory(&b, &c - &b + sizeof(c));
}
main.c
#define _embed(a, b, c, d) ((d << 24) + (c << 16) + (b << 8) + a)
#define embed(S) _embed(S[0], S[1], S[2], S[3])
int main()
{
char c = 'C';
int i = embed("int ");
short half = '<' + ('>' << 8);
long L = embed("l o ") + (((long)embed("n g ")) << 32);
foo(c, half, i, L, half, c);
}
Let's add a line in main.c
to affect how it calls foo:
void foo(char, short, int, long, short, char);
void foo(char, ...);
We now have nominal.c
, forgot.c
and variadic.c
CFLAGS += -Wall -Wextra
CPPFLAGS = -I ~/Workspace/Libft/include
LDFLAGS = -L ~/Workspace/Libft
LDLIBS = -lft
SCENARIOS = nominal forgot variadic
all: $(SCENARIOS)
$(SCENARIOS): foo.o
clean:
$(RM) $(SCENARIOS) *.o
$ uname -mpsr
Linux 5.15.0-52-generic x86_64 x86_64
$ cc --version
cc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0
$ make
cc -Wall -Wextra -I ~/Workspace/Libft/include -c -o foo.o foo.c
cc -Wall -Wextra -I ~/Workspace/Libft/include -L ~/Workspace/Libft nominal.c foo.o -lft -o nominal
cc -Wall -Wextra -I ~/Workspace/Libft/include -L ~/Workspace/Libft forgot.c foo.o -lft -o forgot
forgot.c: In function ‘main’:
forgot.c:11:5: warning: implicit declaration of function ‘foo’ [-Wimplicit-function-declaration]
11 | foo(c, half, i, L, half, c);
| ^~~
cc -Wall -Wextra -I ~/Workspace/Libft/include -L ~/Workspace/Libft variadic.c foo.o -lft -o variadic
$ ./nominal
0 : 67
4 : 15932
8 : 544501353
20 : 2334870589177536620
12 : 15932
24 : 67
437f 0000 6c20 6f20 6e20 6720 3c3e 0000 C...l o n g <>..
696e 7420 3c3e ebbf 43 int <>..C
$ # Interesting how arguments can be out of order
$ # Exactly identical results for the other scenarios
$ uname -mpsr
Darwin 22.1.0 arm64 arm
$ cc --version
Apple clang version 14.0.0 (clang-1400.0.29.102)
Target: arm64-apple-darwin22.1.0
$ make
cc -Wall -Wextra -I ~/Workspace/Libft/include -c -o foo.o foo.c
cc -Wall -Wextra -I ~/Workspace/Libft/include -L ~/Workspace/Libft nominal.c foo.o -lft -o nominal
cc -Wall -Wextra -I ~/Workspace/Libft/include -L ~/Workspace/Libft variadic.c foo.o -lft -o variadic
$ ./nominal
0 : 67
3 : 15932
7 : 544501353
15 : 2334870589177536620
17 : 15932
18 : 67
433c 3e6c 206f 206e 2067 2069 6e74 203c C<>l o n g int <
3e00 43 >.C
$ # "forgot" scenario won't compile
$ ./variadic
0 : 67
3 : -18496
7 : 1796126672
15 : 6091094264
17 : -20520
18 : 44
2cd8 aff8 b80e 6b01 0000 00d0 b70e 6bc0 ,.....k.......k.
b700 43 ..C
$ # As we can see, arguments are not present where the callee expects them to be !
$ # (Except `c`, the only non variadic argument)
Let's now see what appends when the callee is actually variadic on Apple Silicon
foo.c
#include <stdio.h>
#include <stdarg.h>
#include "libft.h"
void foo(char c, ...)
{
va_list args;
va_start(args, c);
char *start = args; printf("[%3ti] %20hhi\n", &c - start , c);
/* Default argument promotion must be taken into account when using va_arg */
short h = va_arg(args, int); printf("[%3ti] %20hi\n", args - start, h);
int i = va_arg(args, int); printf("[%3ti] %20i\n", args - start, i);
long L = va_arg(args, long); printf("[%3ti] %20li\n", args - start, L);
short s = va_arg(args, int); printf("[%3ti] %20hi\n", args - start, s);
char b = va_arg(args, int); printf("[%3ti] %20hhi\n", args - start, b);
ft_print_memory(start, args - start);
va_end(args);
}
$ ./variadic
[-17] 67
[ 8] 15932
[ 16] 544501353
[ 24] 2334870589177536620
[ 32] 15932
[ 40] 67
3c3e 0000 0000 0000 696e 7420 0000 0000 <>......int ....
6c20 6f20 6e20 6720 3c3e 0000 0000 0000 l o n g <>......
4300 0000 0000 0000 C.......
Yep, as announced in their documentation linked before, each argument is put in a 8-bytes slot.
For other scenarios, as the main doesn't know foo is variadic, nothing good happens, e.g. :
[-17] 67
[ 8] 0
[ 16] 544153708
[ 24] 4484459333298453268
[ 32] 28265
[ 40] -96
0000 0000 0000 0000 6c20 6f20 6e20 6720 ........l o n g
147b f002 0100 3c3e 696e 7420 0100 0043 .{....<>int ...C
a0b7 ef6c 0100 0000 ...l....
The reinterpret casts seem to be at fault