Reputation: 2038
I'm implementing a printk
function for my toy OS kernel targeting x86 platform. If I call printk
like this:
uint64_t x = 0xdead;
uint64_t z = 0xbeef;
printk("%p %s\n", x & z, "yes");
that is, passing a 64-bit integer (x & z
) to it, then the generated assembly code would be:
c01000bd: c7 45 f0 ad de 00 00 movl $0xdead,-0x10(%ebp)
c01000c4: c7 45 f4 00 00 00 00 movl $0x0,-0xc(%ebp)
c01000cb: c7 45 e8 ef be 00 00 movl $0xbeef,-0x18(%ebp)
c01000d2: c7 45 ec 00 00 00 00 movl $0x0,-0x14(%ebp)
c01000d9: 8b 45 f0 mov -0x10(%ebp),%eax
c01000dc: 23 45 e8 and -0x18(%ebp),%eax
c01000df: 89 c3 mov %eax,%ebx
c01000e1: 8b 45 f4 mov -0xc(%ebp),%eax
c01000e4: 23 45 ec and -0x14(%ebp),%eax
c01000e7: 89 c6 mov %eax,%esi
c01000e9: 68 e4 17 10 c0 push $0xc01017e4
c01000ee: 56 push %esi
c01000ef: 53 push %ebx
c01000f0: 68 e8 17 10 c0 push $0xc01017e8
c01000f5: e8 27 0a 00 00 call c0100b21 <printk>
You can see here gcc uses two 32-bit registers (%esi
and %ebx
) to store the 64-bit value. The result of this is the number of arguments pushed to the stack becomes 4 instead of 3. If the code inside printk
cannot figure out the size of the argument and consume it properly, the stack access would be messed up.
So my question is, when implementing a variadic function, how can I know the size of the next argument when I use va_arg
macro? Or specifically, how to solve this 32-bit-vs-64-bit printk
problem?
Upvotes: 1
Views: 308
Reputation: 4079
C does not provide a general way to determine the size or type of the arguments. For printf
like functions you must rely on the format string. This is why getting the format string wrong compared to the arguments passed can result in some very buggy code; since you can be reading past the intended arguments if you get the size wrong. The format string also tells you the number of arguments.
Also another thing to consider is the default argument promotion for variadic functions. You can find more information about that in this SO question.
Here is an example of what something going back if the format string mismatches the types you pass via the arguments. You need to compile this on a 32 bit machine or use gcc -m32 file.c
:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
printf("%u: %s\n", (1ULL << 63), "Hello World");
return 0;
}
So why is this so bad, it looks okay, we have 2 format characters and 2 arguments right? Well not so fast, for a 32 bit machine %u
is 32 bits and so is the char pointer. However (1ULL << 63) is 64 bits long. So what happens is that 96 bytes are pushed onto the stack (or however arguments are passed). The format string though is only going to use the first 64. It also just so happens that the first 32 bits will be all zeros and the second 32 bits, the bits used for the char pointer, have the value (1 << 31). Since printf
is expecting a char pointer that value is dereferenced which causes undefined behavior, specifically a segmentation fault on my machine.
Upvotes: 3
Reputation: 1057
You have to use one of the arguments to determine the size (usually the first argument). With printf() the size is determined by the format specifiers in the format string. There's nothing really magical about this.
Upvotes: 1