agagniere / Libft

Implementation of standard functions
MIT License
4 stars 0 forks source link

ft_printf misbehaves on Apple Silicon #21

Closed agagniere closed 1 year ago

agagniere commented 1 year ago

The reinterpret casts seem to be at fault

agagniere commented 1 year ago

Variadic C functions implementation details

Apple Silicon

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:

  1. Round up the Next SIMD and Floating-point Register Number (NSRN) to the next multiple of 8 bytes.
  2. 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)

Amd64

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];
agagniere commented 1 year ago

TL;DR

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

agagniere commented 1 year ago

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 );
agagniere commented 1 year ago

Since we're here

Interesting reads :

Having some fun

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);
}

Scenarios

Let's add a line in main.c to affect how it calls foo:

We now have nominal.c, forgot.c and variadic.c

Makefile

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

Results

$ 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)
agagniere commented 1 year ago

The other way around

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);
}

Output

$ ./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....