Search code examples
cstringvariadic-functionselfunsigned-integer

va_args not accepting signed integer in C


I was designing my kernel using C. While making the kprintf function (a function like printf but works with the kernel) I saw that signed integers (precisely the data type is long), va_args is converting them to unsigned long.

Here's a snippet of the code: kPrint.c

#include <stdarg.h>

// skipped some lines not needed for this question
// ...

/******************
 * global variables needed for number to string conversion
 * ***************/
// store the converted string temporarily
static char convertedString[30];

// store the index of the filled portion
static char numberIndex;

// TODO: Create function like printf but which works here
_Bool kprintf(const char* stringFormat, ...) {
    char* strPtr = (char*)stringFormat;
    va_list arguments;
    va_start(arguments, stringFormat);

    while (*strPtr != 0) {

        switch (*strPtr)
        {
        case '\e':
            // printf() also ends the execution
            return 1;
        case '%':
            switch (*(strPtr + 1))
            {
            // signed decimal integer
            case 'd':
            case 'i':
                kprintf(IntToString(va_arg(arguments, long long))); // stringify the integer
                // *problem* here
                strPtr += 2;
                break;

            // unsigned decimal integer
            case 'u':
                kprintf(uIntToString(va_arg(arguments, uint64_t))); // stringify
                strPtr += 2;
                break;

            // will implement U & L case later
            // unsigned hexadecimal
            case 'x':
            case 'X':
                kprintf(uHexToString(va_arg(arguments, uint64_t))); // stringify
                // doesn't work now
                strPtr += 2;
                break;

            // will implement U & L case later
            // 6 numbers after decimal
            case 'f':
            case 'F':
                kprintf(DoubleToString(va_arg(arguments, double), 6)); // stringify
                // doesn't work now
                strPtr += 2;
                break;

            // 2 numbers after pt
            case 'g':
            case 'G':
                kprintf(DoubleToString(va_arg(arguments, double), 2));
                strPtr += 2;
                break;

            case 'c':
                kPrintChar((char)va_arg(arguments, int));
                //
                strPtr += 2;
                break;

            // another string
            case 's':
                kPrintfHelper(va_arg(arguments, char*)); // just to prevent recursion
                strPtr += 2;
                break;

            case '%':
                kPrintChar('%');
                strPtr += 2;
                break;
            }
            break;
        default:
            kPrintChar(*strPtr);
            strPtr++;
        }
    }
    va_end(arguments);
    return 0;
}

void uIntToStrHelper(uint64_t num) {
    if (num < 10)
    {
        convertedString[numberIndex++] = num + '0';
        convertedString[numberIndex] = 0;
        return;
    }

    uint8_t numIndexCpy = numberIndex;

    while (num > 0) {
        convertedString[numberIndex++] = (num % 10) + '0';
        num /= 10;
    }

    char swpIndex = (numberIndex - 2 + numIndexCpy) / 2;
    numberIndex = numberIndex - swpIndex - 1 + numIndexCpy;

    while (swpIndex >= numIndexCpy) {
        convertedString[swpIndex] = convertedString[swpIndex] + 
            convertedString[numberIndex];

        convertedString[numberIndex] = convertedString[swpIndex] -
            convertedString[numberIndex];

        convertedString[swpIndex] = convertedString[swpIndex] -
            convertedString[numberIndex];
        
        swpIndex--;
        numberIndex++;
    }

    convertedString[numberIndex] = 0;
}

char* uIntToString(uint64_t num) {
    numberIndex = 0;
    uIntToStrHelper(num);
    return convertedString;
}

char* IntToString(long long num) {
    numberIndex = 0;
    if (num < 0) {
        convertedString[numberIndex++] = '-';
        num = -num;
    }

    uIntToStrHelper(num);
    return convertedString;
}

EDIT: Added IntToString and uIntToString.

(I don't know the correct way to do this, but it suffices)

Overview of the problem: cases for 'd' and 'i' show the place of the problem.
Function prototype of IntToString:

char* IntToString(long long num);
// part of the reason why I used long long as va_arg data type
// had also tried with long but with no luck :(

I have tried IntToString & uIntToString functions on a linux machine and they work the way intended.

NOTE: I have not tested kprintf(const char* stringFormat, ...) in a controlled & easily debuggable environment like the other 2 functions mentioned just above.

Other functions like DoubleToString and uHexToString have not yet been tested but it should not change/be related to the question anyway.

kPrintfHelper is a function just like kprintf but does not do any checking and just prints the string

My Environment
Compiler: x86_64-elf-gcc
Flags (aka CFLAGS): -ffreestanding -mno-red-zone -m64

Linker: x86_64-elf-ld

I use qemu-system-x86_64 to run the final executable.

Tests

I have tried the following cases:

  1. Normal unsigned case
kprintf("Number: %d\n", 1234);

Output:

Number: 1234

  1. Does not work but the output seems like va_args is doing some weird unsigned int math (further proved by test 5 that it was indeed va_args, IMO)
kprintf("Number: %d\n", -1234);

Output:

Number: 4294966062

  1. Works as intended
kprintf("Number: %d\n", 1099511627775);

Output:

Number: 1099511627775

  1. BUT This
kprintf("Number: %d\n", -1099511627775);

Output:

Number: 1099511627775

  1. AND here
kprintf("Number: %s\n", IntToString(-1234));

Output:

Number: -1234


Solution

  • The call kprintf("Number: %d\n", -1234); is incorrect because %d extracts a long long. It must be kprintf("Number: %d\n", -1234LL);.

    -1234 is a 32 bit operand. The problem could be that this is being passed in a 64 bit aligned word, but not being sign extended to 64 bits.

    So that is to say, the -1234 value in 64 bits needs to be fffffffffffffb2e, but the 32 bit parameter is producing a 00000000fffffb2e image on the stack, which is 4294966062.

    According to this hypothesis, we would have to pass -1000 to obtain the observed 429496629, however. It bears no relation to -1234. Something else could be going on, like garbage bits being interpreted as data.

    The behavior is not well-defined, after all: you're shoving an integer of one size into a completely typeless and unsafe parameter passing mechanism and pulling out an integer of a different size.