Printing 64-bit decimal numbers in freestanding environment
How can I print 64-bit decimal numbers in freestanding environment?
  suggests me some improvements...


Create test cases

Post lacks a good way to assess its correctness.  Since the goal is to print rather than form a string, that is later printed, (a better function IMO), we will do things the simply, but a bit the hard way and visually compare output.

Test case code

#include &lt;inttypes.h&gt;
#include &lt;limits.h&gt;
#include &lt;stdint.h&gt;
#include &lt;stdio.h&gt;

void print_uint64_dec(uint64_t u) {
  if (u &gt;= 10) {
    uint64_t udiv10 = u/10;
    u %= 10;
    print_uint64_dec(udiv10);
  }
  putchar((int)u + '0');
}

void print_int64_dec(int64_t i) {
  uint64_t u = (uint64_t) i;
  if (i &lt; 0) {
    putchar('-');
    u = -u;
  }
  print_uint64_dec(u);
}

int main(void) {
  int64_t ia[] = {INT64_MIN + 1, 0, 10, INT64_MAX/2, INT64_MAX - 1};
  for (size_t index = 0; index&lt; sizeof ia/sizeof ia[0]; index++) {
    int64_t i = ia[index];
    print_int64_dec(i-1); putchar('\n');
    print_int64_dec(i+0); putchar('\n');
    print_int64_dec(i+1); putchar('\n');
  }
}


And its output:

-9223372036854775808
-9223372036854775807
-9223372036854775806
-1
0
1
9
10
11
4611686018427387902
4611686018427387903
4611686018427387904
9223372036854775805
9223372036854775806
9223372036854775807


OP code produced the same results.



Plan for the future

"processor doesn't have 64 bit division, and I can't rely on libgcc" --> accepting this as a given, this curious restriction could easily be removed later.  To that end, recommend to encapsulate this specialized code - and only the specialized part needed - the divide and mod of a int64_t, without the print part.

Something like

static unsigned mod10(uint64_t *u) {
  uint64_t dividend = *u;
  uint64_t quo = 0;
  uint64_t div = 0xA000000000000000; //largest shifted 10
  while (div &gt;= 10) {
    quo &lt;&lt;= 1; // quo *= 2;
    if (dividend &gt;= div) {
      dividend -= div;
      quo++;
    }
    div &gt;&gt;= 1;
  }
  *u = quo;
  return (unsigned) dividend;
}

void print_uint64_dec(uint64_t u) {
  if (u &gt;= 10) {
    #if CURIOUS_CODE_NEEDED
      uint64_t udiv10 = u;
      u = mod10(&amp;udiv10);
    #else
      uint64_t udiv10 = u/10;
      u %= 10;
    #endif
    print_uint64_dec(udiv10);
  }
  putchar((int)u + '0');
}




Simplify

I just had too much trouble following the "invented other way".  I recommend to  re-write to only replace the missing /10, mod 10 functionality in  a stand-alone function.  When a target platform lacks standard functionality, do not entangle your fill-in-the-gap code with the rest of user code.  Keep them separate.



Minor - Use consistent types

Code   converts intmax_t to uint64_t as if  intmax_t is always a 64-bit type.  (u)intmax_t is at least a 64-bit type.  Choose (u)intmax_t or (u)int64_t.

Minor - Avoid UB

intmax_t num ... -num is UB when num == INTMAX_MIN.  Simply enough to fix. Negate an unsigned type

// uint64_t n = num &lt; 0 ? -num : +num;
uint64_t n = num;
if (num &lt; 0) n = -n;
// OR
uint64_t n = num &lt; 0 ? -(uintmax_t)num : num;
// Or various other approaches.


Minor - Missing  headers

I needed the below to compile your code.

#include &lt;limits.h&gt;
#include &lt;stdint.h&gt;