As other have pointed out, it is entirely undefined behaviour, and what you get will depend on the compiler. It will work only if you have a specific call convention, that doesn't use the stack but registers to pass the parameters.
I used Godbolt to see the assembly generated, that you can check in full here
The relevant function call is here:
mov edi, 10
mov esi, 20
mov edx, 30
call f(int, int) #clang totally knows you're calling f by the way
It doesn't push parameters on the stack, it simply puts them in registers. What is most interesting is that the mov
instruction doesn't change just the lower 8 bits of the register, but all of them as it is a 32-bit move. This also means that no matter what was in the register before, you will always get the right value when you read 32 bits back as f does.
If you wonder why the 32-bit move, it turns out that in almost every case, on a x86 or AMD64 architecture, compilers will always use either 32 bit literal moves or 64 bit literal moves (if and only if the value is too big for 32 bits). Moving a 8 bit value doesn't zero out the upper bits (8-31) of the register, and it can create problems if the value would end up being promoted. Using a 32-bit literal instruction is more simple than having one additional instruction to zero out the register first.
One thing you have to remember though is it is really trying to call f
as if it had 8 bits parameters, so if you put a large value it will truncate the literal. For example, 1000
will become -24
, as the lower bits of 1000
are E8
, which is -24
when using signed integers. You will also get a warning
<source>:13:7: warning: implicit conversion from 'int' to 'signed char' changes value from 1000 to -24 [-Wconstant-conversion]