Using va_list as an array in C - c

Is it safe and defined behaviour to read va_list like an array instead of using the va_arg function?
EX:
void func(int string_count, ...)
{
va_start(valist, string_count);
printf("First argument: %d\n", *((int*)valist));
printf("Second argument: %d\n", *(((int*)valist)+1));
va_end(valist);
}
Same question for assigningment
EX:
void func(int string_count, ...)
{
va_start(valist, string_count);
printf("Third argument: %d\n", *(((int*)valist)+2));
*((int*)valist+2)=33;
printf("New third argument: %d\n", *(((int*)valist)+2));
va_end(valist);
}
PS: This seems to work on GCC

No, it is not, you cannot assume anything because the implementation varies across libraries.
The only portable way to access the values is by using the macros defined in stdarg.h for accessing the
ellipsis. The size of the type is important, otherwise you end up reading garage
and if your read more bytes than has been passed, you have undefined behaviour.
So, to get a value, you have to use va_arg.
See: STDARG documentation
You cannot relay on a guess as to how va_list works, or on a particular
implementation. How va_list works depends on the ABI, the architecture, the
compiler, etc. If you want a more in-depth view of va_list, see
this answer.
edit
A couple of hours ago I wrote this answer explaining how to use the
va_*-macros. Take a look at that.

No, this is not safe and well-defined. The va_list structure could be anything (you assume it is a pointer to the first argument), and the arguments may or may not be stored contiguously in the "right order" in some memory area being pointed to.
Example of va_list implementation that doesn't work for your code - in this setup some arguments are passed in registers instead of the stack, but the va_arg still has to find them.

If an implementation's documentation specifies that va_list may be used in ways beyond those given in the Standard, you may use them in such fashion on that implementation. Attempting to use arguments in other ways may have unpredictable consequences even on platforms where the layout of parameters is specified. For example, on a platform where variadic arguments are pushed on the stack in reverse order, if one were to do something like:
int test(int x, ...)
{
if (!x)
return *(int*)(4+(uintptr_t)&x); // Address of first argument after x
... some other code using va_list.
}
int test2(void)
{
return test(0, someComplicatedComputation);
}
a compiler which is processing test2 might look at the definition of test,
notice that it (apparently) ignores its variadic arguments when the first
argument is zero, and thus conclude that it doesn't need to compute and
pass the result of someComplicatedComputation. Even if the documentation
for the platform documents the layout of variadic arguments, the fact that
the compiler can't see that they are accessed may cause it to conclude that
they are not.

Related

Why does printf require a mandatory parameter?

The definition of printf function in C Language is:
int printf(const char * _Format, ...);
The same is for scanf and a lot of similar functions where is managed a variable number of arguments.
Why is there a _Format mandatory parameter?
The format string is mandatory because the way C's variable argument macros work depends on at least one argument being present, and using it to find the others.
Specifically, to read the other variable arguments, you use va_start (then va_arg repeatedly, once for each variable argument you want to read). When you call va_start, you need to pass it the format string (or, more generally, the last non-varying parameter to the function).
For example, this acts like printf, but prints to both stdout and another file of your choice:
void tee(FILE *f, char const *fmt, ...) {
va_list ap;
va_start(ap, fmt);
vprintf(fmt, ap);
va_end(ap);
va_start(ap, fmt);
vfprintf(f, fmt, ap);
va_end(ap);
}
This uses vprintf and vfprintf, so it doesn't (directly) use va_arg itself, only va_start and va_end, but that's enough to show how the fmt is involved in using va_start.
At one time, this wasn't actually needed. Back when C was shiny and new, you could have a function equivalent to: int f(...);.
During the first C standardization effort, however, this was eliminated in favor of the macros noted above (va_start, va_arg, va_end) that require at least one named parameter. The older macros placed a number of requirements on the calling convention:
Parameters are always passed the same way, regardless of type or number.
It's always easy to find the first argument that was passed.
With the conventional C calling convention (all arguments are passed on the stack, arguments are pushed from right to left) this was true. You basically just looked at the top of the stack, moved backward past the return address, and there was the first argument.
With other calling conventions, things weren't so simple though. For example, just pushing arguments from left to right means that the first argument (the format string, in the case of printf) is buried some arbitrary distance down the stack, with an arbitrary number of other arguments after it.
The way they came up with to deal with that was to pass the immediately previous (named) argument to va_start (and va_start is a macro that will normally use the address of that argument). If you push from right to left, that will give you an address whatever distance needed down the stack, then va_arg can walk back up the stack to retrieve the other variable arguments.
This was apparently seen as an acceptable compromise, especially since functions that take variable arguments almost always take at least one named parameter anyway.
Because it doesn't want to guess what to print
It's mandatory because printf is used to print data. Imagine what'll happen if you print nothing. Nothing. So, why to remove that parameter?
That's the same thing about scanf: you need to read data somehow and how are you going to do it if you don't know the format of this data?
Some functions don't have parameters because they don't need them, eg
void Hello(void) { puts("Hello"); }
So, they can 'survive' without parameters. About printf:
int printf(void) { //imaginary function, don't use it!
// WTF? What to print?
// Absolutely nothing! What's the purpose then?
return smth;
}
Then this printf is absolutely useless when no arguments are passed.
In general, functions that have an unknown number of arguments rely on va_start, va_arg, and va_end to process the arguments that are not explicitly in the function's parameter list.
va_start needs the last named parameter to work with. Hence, a function that has an unknown number of arguments must have at least one named argument.
For printf the parameter/argument that specifies the format specification is the best choice as the required parameter/argument.
Without a format description, printf wouldn't understand what to print. To C, everything is just bytes, so printf has no idea what kind of data is being passed to it, and therefore no idea how to represent it.
When you're new to C, you might not yet realize how true this is, especially if you've learned a language where print() understands the type of data it's seeing.

Pointer to void as an argument in a function with no prototype for variable number of arguments

Say I have a function that should accept any number of parameters, so what im coing here is declaring no prototype, and letting the function to be created when it is called in the code. I am using a pointer to void to receive the random number of parametersparameters, however, when doing this, the reference to the memory addres of the first parameter is the only thing that is passed, so for it to work, i would have to declare variables in the same order that i am going to call them in the code:
unsigned char result=0;
unsigned char a=1;
unsigned char b=2;
unsigned char c=3;
char main (void)
{
for (;;)
{
result = function (&a, &b, &c);
result = function (&c, &b, &a);
}
}
function (void *vPointer)
{
return (1);
}
Also I am declaring function without a type since it would not match the call (where it is implicitly declared also).
The result here is a reference to the first parameter sent in the function, so if i point to the next addres in the first function call, it would work, but in the second call, it gets the reference to c, and whatever memory is ahead of where it is placed.
Anyone know a way of sorting the parameters references the correct way? or an effective way to receive an unknown number of parameters in a function?
NOTE: (...) SHALL NOT be used.
All C functions should have prototypes. They're not actually mandatory, but there's no good reason not to use them (unless you're stuck with a pre-ANSI compiler that doesn't support them). (But see the bottom of this answer.)
If you want a function that takes a variable number of arguments, that prototype should end with , ..., and the function itself should use the <stdarg.h> mechanism to process its arguments. (This requires at least one argument with a defined type; that argument is used as an anchor for the following arguments.) It's documented here and elsewhere.
As I was typing this, you updated your question with "NOTE: No libraries (such as (...) )should be used". <stdarg.h> is one of the handful headers that's required for all conforming C implementations, including freestanding (embedded) ones -- because it doesn't define any functions, just types and macros. Your C implementation should support it. If it doesn't, then it's not a conforming C implementation, and you'll need to tell us exactly what compiler you're using and/or read its documentation to find out how it handles variadic functions, or an equivalent.
If you really can't use , ... and <stdarg.h>, (or perhaps the older <varargs.h>), then you can define your function with a fixed number of arguments, enough for all uses, then have callers pass extra null pointers.
EDIT:
This is an update based on new information in comments and chat.
The OP has a homework assignment to implement printf for some TI microcontroller, for some reason not using either the , ... notation or <stdarg.h>. The compiler in question apparently implements C89/C90, so it does support both features; this is an arbitrary restriction.
This information should have been in the question, which is why I'm downvoting it until the OP updates it.
There is no portable way to achieve this -- which is exactly why , ... is part of the standard language, and <stdarg.h> is part of the standard library.
Probably the best approach would be to write a program that uses , ... and <stdarg.h>, then invoke the compiler so it shows just the output of the preprocessor (resolving the various va_* macros and the va_list type), and then imitate that. And you'd have to assume, or verify using the compiler documentation, that the calling convention for variadic and non-variadic functions is compatible. In other words, find out what this particular implementation does, and reinvent a similar wheel.
(I hope that the point of the homework assignment is to demonstrate how much better the standard techniques are.)
UPDATE 2:
I wrote above that all C functions should have prototypes. This may actually be a rare exception to this rule. At least one of these calls:
printf("Hello\n");
printf("x = %d\n", 42);
must produce a diagnostic from a conforming compiler unless either printf is declared with , ... (which is forbidden by the homework assignment), or there is no visible prototype for printf. If there's no prototype, then at least one of the calls will have undefined behavior (behavior that's not defined by the C standard, though it may be defined by a particular compiler).
In effect, to meet the homework requirements, you'll have to pretend that you're using a pre-ANSI C compiler.
the only "clean" way to use functions with variable arguments is to use variadic functions:
#include <stdarg.h>
void myfun(int foo, ...) {
va_list ap;
va_start(foo, ap);
// ...
va_end(ap);
}
you will need to make sure that you know which arguments you actually expect (usually you either use your first argument to indicate how many (and which) arguments to expect (examples are an int that says "now come arguments", or a format-string like "%d %s:%s", that says now come an int and two char*), or you use a a final terminating argument (e.g. read arguments until you encounter NULL).
You could use an array of variable length:
unsigned char result=0;
unsigned char a=1;
unsigned char b=2;
unsigned char c=3;
function (int len, void *vPointer);
int main (void)
{
for (;;)
{
unsigned char args[3];
args[0] = a;
args[1] = b;
args[2] = c;
result = function (3, args);
args[0] = c;
args[1] = b;
args[2] = a;
result = function (3, args);
}
return 0;
}
function (int len, void *vPointer)
{
return (1);
}
But I recommend you use the standard way instead, i.e. variadic functions.
//jk
You can use a structure:-
typedef struct _Params {
int m_a;
int m_b;
int m_c;
} Params;
Then your parameters can't get mixed up. Just as more letters up to the max you need.

In which cases va_list should be used

I made a small C library that implements graph theory algorithms and binds them for use in Python.
I send it to a friend to check it and he told me that va_list is "dangerous" and must not be used in this kind of project.
So the question is. In which cases va_list should be used?
The main problem I see is that there's no guarantee that you really got the number of arguments that you were expecting, and no way to check for that. This makes errors undetectable, and undetectable errors are, obviously, the most dangerous kind. va_arg is also not type-safe, which means that if you pass a double and expect an unsigned long long, you'll get garbage instead of a good-looking integer, and no way to detect it at compile-time. (It becomes much more of a mess when the types don't even have the same size).
Depending on the data you deal with, this may be more or less of a problem. If you pass pointers, it becomes almost instantly fatal to omit an argument because your function will retrieve garbage instead, and this could (if the planets are properly aligned) become a vulnerability.
If you pass "regular" numeric data, it then depends on if the function is critical. In some cases you can easily detect an error looking at the function's output, and in some practical cases it really isn't that much of a problem if the function fails.
It all revolves about if you're afraid of forgetting arguments yourself, actually.
C++11 has a variadic template feature that allows you to treat an arbitrary number of parameters in a safe way. If the step from C to C++ isn't hurting too much, you could look into it.
In C++11, va_list should never be used, as it provides better alternative called variadic template, which is typesafe whereas va_list is not.
In C, you could use va_list when you need variadic function, but be careful, as it is not typesafe.
And yes, your friend is correct: va_list is dangerous. Avoid it as much as possible.
In C and C++03, the standard library function printf is implemented using va_list, that is why C++03 programmers usually avoid using this, for it is not typesafe.
But a variadic typesafe printf could be implemented in C++11, as: (taken from wiki)
void printf(const char *s)
{
while (*s) {
if (*s == '%' && *(++s) != '%')
throw std::runtime_error("invalid format string: missing arguments");
std::cout << *s++;
}
}
template<typename T, typename... Args>
void printf(const char *s, T value, Args... args)
{
while (*s) {
if (*s == '%' && *(++s) != '%') {
std::cout << value;
++s;
printf(s, args...);
return;
}
std::cout << *s++;
}
throw std::logic_error("extra arguments provided to printf");
}
va_list has some disadvanges that are related to the underspecification of the function arguments:
when calling such a function the compiler doesn't know what types of
arguments are expected, so the standard imposes some "usual
conversion" before the arguments are passed to the function. E.g all
integers that are narrower than int are promoted, all float are
promoted to double. In some border case you'd not received what you
wanted in the called function.
In the called function you tell the compiler what type of argument
you expect and how much of them. There is no guarantee that a caller get's it right.
If you pass in the number of arguments anyhow and these are of the same known type you could just pass them in with a temporary array, written for C99:
void add_vertices(graph G, vertex v, size_t n, vertex neigh[n]);
you would call this something like that
add_vertices(G, v, nv, (vertex []){ 3, 5, 6, 7 });
If that calling convention looks too ugly to you, you could wrap it in a macro
#define ADD_VERTICES(G, V, NV, ... ) add_vertices((G), (V), (NV), (vertex [NV]){ __VA_ARG__ })
ADD_VERTICES(G, v, nv, 3, 5, 6, 7);
here the ... indicates a similar concept for macros. But the result is much safer since the compiler can do a type check and this is not delayed to the execution.
If you want to implement a function in C with variable argument count, you can use va_list. For example, printf uses va_list. Not sure why it can be dangerous.

Is declaring an header file essential?

Is declaring an header file essential? This code:
main()
{
int i=100;
printf("%d\n",i);
}
seems to work, the output that I get is 100. Even without using stdio.h header file. How is this possible?
You don't have to include the header file. Its purpose is to let the compiler know all the information about stdio, but it's by no means necessary if your compiler is smart (or lazy).
You should include it because it's a good habit to get into - if you don't, then the compiler has no real way to know if you're breaking the rules, such as with:
int main (void) {
puts (7); // should be a string.
return 0;
}
which compiles without issue but rightly dumps core when running. Changing it to:
#include <stdio.h>
int main (void) {
puts (7);
return 0;
}
will result in the compiler warning you with something like:
qq.c:3: warning: passing argument 1 of ‘puts’ makes pointer
from integer without a cast
A decent compiler may warn you about this, such as gcc knowing about what printf is supposed to look like, even without the header:
qq.c:7: warning: incompatible implicit declaration of
built-in function ‘printf’
How is this possible? In short: three pieces of luck.
This is possible because some compilers will make assumptions about undeclared functions. Specifically, parameters are assumed to be int, and the return type also int. Since an int is often the same size as a char* (depending on the architecture), you can get away with passing ints and strings, as the correct size parameter will get pushed onto the stack.
In your example, since printf was not declared, it was assumed to take two int parameters, and you passed a char* and an int which is "compatible" in terms of the invocation. So the compiler shrugged and generated some code that should have been about right. (It really should have warned you about an undeclared function.)
So the first piece of luck was that the compiler's assumption was compatible with the real function.
Then at the linker stage, because printf is part of the C Standard Library, the compiler/linker will automatically include this in the link stage. Since the printf symbol was indeed in the C stdlib, the linker resolved the symbol and all was well. The linking was the second piece of luck, as a function anywhere other than the standard library will need its library linked in also.
Finally, at runtime we see your third piece of luck. The compiler made a blind assumption, the symbol happened to be linked in by default. But - at runtime you could have easily passed data in such a way as to crash your app. Fortunately the parameters matched up, and the right thing ended up occurring. This will certainly not always be the case, and I daresay the above would have probably failed on a 64-bit system.
So - to answer the original question, it really is essential to include header files, because if it works, it is only through blind luck!
As paxidiablo said its not necessary but this is only true for functions and variables but if your header file provides some types or macros (#define) that you use then you must include the header file to use them because they are needed before linking happens i.e during pre-processing or compiling
This is possible because when C compiler sees an undeclared function call (printf() in your case) it assumes that it has
int printf(...)
signature and tries to call it casting all the arguments to int type. Since "int" and "void *" types often have same size it works most of the time. But it is not wise to rely on such behavior.
C supprots three types of function argument forms:
Known fixed arguments: this is when you declare function with arguments: foo(int x, double y).
Unknown fixed arguments: this is when you declare it with empty parentheses: foo() (not be confused with foo(void): it is the first form without arguments), or not declare it at all.
Variable arguments: this is when you declare it with ellipsis: foo(int x, ...).
When you see standard function working then function definition (which is in form 1 or 3) is compatible with form 2 (using same calling convention). Many old std. library functions are so (as desugned to be), because they are there form early versions of C, where was no function declarations and they all was in form 2. Other function may be unintentionally be compatible with form 2, if they have arguments as declared in argument promotion rules for this form. But some may not be so.
But form 2 need programmer to pass arguments of same types everywhere, because compiler not able to check arguments with prototype and have to determine calling convention osing actual passed arguments.
For example, on MC68000 machine first two integer arguments for fixed arg functions (for both forms 1 and 2) will be passed in registers D0 and D1, first two pointers in A0 and A1, all others passed through stack. So, for example function fwrite(const void * ptr, size_t size, size_t count, FILE * stream); will get arguments as: ptr in A0, size in D0, count in D1 and stream in A1 (and return a result in D0). When you included stdio.h it will be so whatever you pass to it.
When you do not include stdio.h another thing happens. As you call fwrite with fwrite(data, sizeof(*data), 5, myfile) compiler looks on argruments and see that function is called as fwrite(*, int, int, *). So what it do? It pass first pointer in A0, first int in D0, second int in D1 and second pointer in A1, so it what we need.
But when you try to call it as fwrite(data, sizeof(*data), 5.0, myfile), with count is of double type, compiler will try to pass count through stack, as it is not integer. But function require is in D1. Shit happens: D1 contain some garbage and not count, so further behaviour is unpredictable. But than you use prototype defined in stdio.h all will be ok: compiler automatically convert this argument to int and pass it as needed. It is not abstract example as double in arument may be just result of computation involving floating point numbers and you may just miss this assuming result is int.
Another example is variable argument function (form 3) like printf(char *fmt, ...). For it calling convention require last named argument (fmt here) to be passed through stack regardess of its type. So, then you call printf("%d", 10) it will put pointer to "%d" and number 10 on stack and call function as need.
But when you do not include stdio.h comiler will not know that printf is vararg function and will suppose that printf("%d", 10) is calling to function with fixed arguments of type pointer and int. So MC68000 will place pointer to A0 and int to D0 instead of stack and result is again unpredictable.
There may be luck that arguments was previously on stack and occasionally read there and you get correct result... this time... but another time is will fail. Another luck is that compiler takes care if not declared function may be vararg (and somehow makes call compatible with both forms). Or all arguments in all forms are just passed through stack on your machine, so fixed, unknown and vararg forms are just called identically.
So: do not do this even you feel lucky and it works. Unknown fixed argument form is there just for compatibility with old code and is strictly discouraged to use.
Also note: C++ will not allow this at all, as it require function to be declared with known arguments.

Why does this variadic function fail on 4th parameter on Windows x64?

Below is code which includes a variadic function and calls to the variadic function. I would expect that it would output each sequence of numbers appropriately. It does when compiled as a 32-bit executable, but not when compiled as a 64-bit executable.
#include <stdarg.h>
#include <stdio.h>
#ifdef _WIN32
#define SIZE_T_FMT "%Iu"
#else
#define SIZE_T_FMT "%zu"
#endif
static void dumpargs(size_t count, ...) {
size_t i;
va_list args;
printf("dumpargs: argument count: " SIZE_T_FMT "\n", count);
va_start(args, count);
for (i = 0; i < count; i++) {
size_t val = va_arg(args, size_t);
printf("Value=" SIZE_T_FMT "\n", val);
}
va_end(args);
}
int main(int argc, char** argv) {
(void)argc;
(void)argv;
dumpargs(1, 10);
dumpargs(2, 10, 20);
dumpargs(3, 10, 20, 30);
dumpargs(4, 10, 20, 30, 40);
dumpargs(5, 10, 20, 30, 40, 50);
return 0;
}
Here is the output when compiled for 64-bit:
dumpargs: argument count: 1
Value=10
dumpargs: argument count: 2
Value=10
Value=20
dumpargs: argument count: 3
Value=10
Value=20
Value=30
dumpargs: argument count: 4
Value=10
Value=20
Value=30
Value=14757395255531667496
dumpargs: argument count: 5
Value=10
Value=20
Value=30
Value=14757395255531667496
Value=14757395255531667506
Edit:
Please note that the reason the variadic function pulls size_t out is because the real-world use of this is for a variadic function that accepts a list of pointers and lengths. Naturally the length argument should be a size_t. And in some cases a caller might pass in a well-known length for something:
void myfunc(size_t pairs, ...) {
va_list args;
va_start(args, count);
for (i = 0; i < pairs; i++) {
const void* ptr = va_arg(args, const void*);
size_t len = va_arg(args, size_t);
process(ptr, len);
}
va_end(args);
}
void user(void) {
myfunc(2, ptr1, ptr1_len, ptr2, 4);
}
Note that the 4 passed into myfunc might encounter the problem described above. And yes, really the caller should be using sizeof or the result of strlen or just plain put the number 4 into a size_t somewhere. But the point is that the compiler is not catching this (a common danger with variadic functions).
The right thing to do here is to eliminate the variadic function and replace it with a better mechanism that provides type safety. However, I would like to document this problem, and collect more detailed information as to exactly why this problem exists on this platform and manifests as it does.
So basically, if a function is variadic, it must conform to a certain calling convention (most importantly, the caller must clean up args, not the callie, since the callie has no idea how many args there will be).
The reason why it starts happening on the 4th is because of the calling convention used on x86-64. To my knowledge, both visual c++ and gcc use registers for the first few parameters, and then after that use the stack.
I am guessing that this is the case even for variadic functions (which does strike me as odd since it would make the va_* macros more complicated).
On x86, the standard C calling convention is the use the stack always.
The problem is that you're using size_t to represent the type of the values. This is incorrect, the values are actually normal 32 bit values on Win64.
Size_t should only be used for values which change size based on the 32 or 64 bit-ness of the platform (such as pointers). Change the code to use int or __int32 and this should fix your problem.
The reason this works fine on Win32 is that size_t is a different sized type depending on the platfrom. For 32 bit windows it will be 32 bits and on 64 bit windows it will be 64 bit. So on 32 bit windows it just happens to match the size of the data type you are using.
A variadic function is only weakly type checked. In particular, the function signature does not provide enough information for the compiler to know the type of each argument assumed by the function.
In this case, size_t is 32-bits on Win32 and 64-bits on Win64. It has to vary in size like that in order to perform its defined role. So for a variadic function to pull arguments out correctly which are of type size_t, the caller had to make certain that the compiler could tell that the argument was of that type at compile-time in the calling module.
Unfortunately 10 is a constant of type int. There is no defined suffix letter that marks a constant to be of type size_t. You could hide that fact inside a platform-specific macro, but that would be no clearer than writing (size_z)10 at the call site.
It appears to work partially because of the actual calling convention used in Win64. From the examples given, we can tell that the first four integral arguments to a function are passed in registers, and the rest on the stack. That allowed count and the first three variadic parameters to be read correctly.
However it only appears to work. You are actually standing squarely in Undefined Behavior territory, and "undefined" really does mean "undefined": anything can happen.
On other platforms, anything can happen too.
Because variadic functions are implicitly unsafe, a special burden is placed on the coder to make certain that the type of each argument known at compile time matches the type that argument will be assumed to have at run time.
In some cases where the interfaces are well known, it is possible to warn about type mismatch. For example, gcc can often recognize that the type of an argument to printf() doesn't match the format string, and issue a warning. But doing that in the general case for all variadic functions is hard.
The reason for this is because size_t is defined as a 32-bit value on 32-bit Windows, and a 64-bit value on 64-bit Windows. When the 4th argument is passed into the variadic function, the upper bits appear to be uninitialized. The 4th and 5th values that are pulled out are actually:
Value=0xcccccccc00000028
Value=0xcccccccc00000032
I can solve this problem with a simple cast on all the arguments, such as:
dumpargs(5, (size_t)10, (size_t)20, (size_t)30, (size_t)40, (size_t)50);
This does not answer all my questions, however; such as:
Why is it the 4th argument? Likely because the first 3 are in registers?
How does one avoid this situation in a type-safe portable manner?
Does this happen on other 64-bit platforms, using 64-bit values (ignoring that size_t might be 32-bit on some 64-bit platforms)?
Should I pull out the values as 32-bit values regardless of the target platform, and will that cause problems if a 64-bit value is pushed into the variadic function?
What do the standards say about this behavior?
Edit:
I really wanted to get a quote from The Standard, but it's something that's not hyperlink-able, and costs money to purchase and download. Therefore I believe quoting it would be a copyright violation.
Referencing the comp.lang.c FAQ, it's made clear that when writing a function that takes a variable number of arguments, there's nothing you can do for type safety. It's up to the caller to make sure that each argument either perfectly matches or is explicitly cast. There are no implicit conversions.
That much should be obvious to those who understand C and printf (note that gcc has a feature to check printf-style format strings), but what's not so obvious is that not only are the types not implicitly cast, but if the size of the types don't match what's extracted, you can have uninitialized data, or undefined behavior in general. The "slot" where an argument is placed might not be initialized to 0, and there might not be a "slot"--on some platforms you could pass a 64-bit value, and extract two 32-bit values inside the variadic function. It's undefined behavior.
If you are the one writing this function, it is your job to write the variadic function correctly and/or correctly document your function's calling conventions.
You already found that C plays fast-and-loose with types (see also signedness and promotion), so explicit casting is the most obvious solution. This is frequently seen with integer constants being explicitly defined with things like UL or ULL.
Most sanity checks on passed values will be application-specific or non-portable (e.g. pointer validity). You can use hacks like mandating that pre-defined sentinel value(s) be sent as well, but that's not infallible in all cases.
Best practice would be to document heavily, perform code reviews, and/or write unit tests with this bug in mind.

Resources