Here’s a program that exhibits the  low level nature of  C  by changing the return address of a function. The function “one” always returns the number 1 and the function “two” always returns the number 2. The main code calls “one” and prints out that it returned 1, then calls “two”  and then changes its mind and tells us that “one” returned returned 2. The trick is that that the return address for “two” is modified so it returns as if it had been called from the statement that calls “one”.

This program is a good way to learn some machine architecture and also helps with understanding core operating systems code and computer virus coding, but my main excuse is that it is fun. The expressions marked in red are what make the trick work (although the color doesn’t matter -) ). The first version is for X86-64 and the second version is for Arm64 (and your Apple notebook).

/* Irresponsible C Code  (c) Victor Yodaiken 2022/2026
Tested on  AMD Ryzen 5 and compiled with zero optimization
gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0. 
tcc version 0.9.27 (x86_64 Linux)
Ubuntu clang version 18.1.3 (1ubuntu1) x8664
*/

#ifdef GCC // compile with gcc -DGCC
#define OFFSET 3
#else 
#define OFFSET 2
#endif

#include 
int one(void);
int two(void);
void *y = 0; //eventually holds the return address of "one"
int main() 
{ 
        int i;
        if ((i= one()) == 1) {
            printf("one returns 1\n");
            two();
	    printf("The program never gets here\n");
        } else printf("one returns %d\n",i);
	return 0; 
}
int one(void)
{
	char **x = (char **)(&x + OFFSET);
	y = *x;
	return 1; //always returns 1
}
int two(void)
{
	char **x = (char **)(&x + OFFSET); 
	*x = y;
	return 2;
}

This code works on  current generations of tcc, clang, and gcc (as long as you adjust the stack pointer offset and do not, under any circumstances run the optimizer).  If you compile with GCC you have to define the compile time constant GCC, for example by typing the command “gcc X8664.c -DGCC”.  The expression “&x+ OFFSET”  calculated the address of the stack location that holds the return address. On X86, the call subroutine instruction pushes the address of the next instruction onto the stack and then jumps to the subroutine. This code exploits the C compiler allocating space for the variable “x” on the stack, takes that address and looks further up the stack to find the return address. When I was writing this code, I figured out the pointer arithmetic and where to find the return address on the stack by experiment with code something like:

 printf("x=%p *x=%p x+1=%p *(x+1)=%p\n", x,*x,x+1,*(x+1));

extended to 2 and 3 and 4. Also use “objdump –disassemble a.out” on Linux and MacOS to see the assembly code.  This calculation is very sensitive to what else is in the code – adding another variable declaration above the one for “x” or making “one” or “two” take arguments would change the number. Also turning on the so-called “optimizer” breaks this code because current  both Clang and GCC  can change the semantics of C code in often unpredictable ways when they are supposedly optimizing .  This is permitted by the C Standard  for reasons that strike me as not well thought out, but let’s pass over the problem for now (or see this )

For Arm64, the call subroutine instruction puts the return address into a link register, and not on the stack. So, in simple code, we need to copy and save the link register that “one” gets and then “two” can just load that value into the register. Unfortunately, C has not evolved to allow modification of registers in C code, but C’s inline assembler does the job.

/* Irresponsible C Code  (c) Victor Yodaiken 2022/2026
Tested on Apple M3 Macbook
Apple clang version 17.0.0 (clang-1700.0.13.5) 
gcc-14 (Homebrew GCC 14.2.0_1) 14.2.0
*/

#include <stdio.h>

int one(void);
int two(void);
int main() // On Arm-64 executes both printfs
{
    int i;
    printf("Main\n");

        if ((i= one()) == 1) {
            printf("one returns 1\n");
            two();
            printf("The program never gets here\n");
        } else printf("one returns %d\n",i);

        return 0;
}
void *y = 0; //will hold the return address of "one"

int one(void)
{
asm volatile("mov  %0,lr": "=r" (y)); //save lr (link register) in y
return 1; //always returns 1 
}
int two(void) //always returns 2
{
asm volatile("mov lr,%0 " :: "r" (y)); //load lr from y
return 2;
}

This solution is just as fragile as the X86 solution. Although putting the return value in the link register instead of pushing it onto the stack seems like a cleaner and more modern method, if the called subroutine calls a second subroutine, that link register  has to be saved and restored when the second subroutine returns. Where is it saved? On the stack, like they did in old days. So if you add, say “printf” to “two” before the return, the link register will be saved on the stack and then popped back out before the return – overwriting the value we used from “y” (not that I would have made such an obvious error ).

There is a famous fragment of code in early UNIX kernel versions that uses the same stack  trick used in these examples. It goes something like this:

/* you are not expected to understand this*/
next = find_next_process_to_run();
if(save()){
           current_process = next;
           resume();
           panic("process switch failed catastrophically\n");
           }

“Save” saves the context of the current process in a structure pointed to by “current_process” and returns a non-zero value while “resume” restores state from “current_process”  and then returns 0. The trick is that “save” saves the return address among other things and “resume”  returns to the condition test.   You may be  not expected to understand this,  as Dennis Ritchie wrote, but when you do, you know a lot about how process or thread switching actually works.

Retracted: The next chapter of buy priligy priligy online Unforgivable C  is on the way in about 2 weeks, I hope.

Unforgivable C programming 1
Tagged on:                 

2 thoughts on “Unforgivable C programming 1

  • September 15, 2022 at 9:33 am
    Permalink

    Hi Victor,

    I’m not sure what this example demonstrates, because it’s not conforming C code. The pointer addition &x+2 is undefined behavior. Annex J.2 of the C99 standard mentions the situation: “Addition or subtraction of a pointer into, or just beyond, an array object and an integer type produces a result that does not point into, or just beyond, the same array object (6.5.6).”

    While the example may show the behavior of certain compilers when fed invalid code, it doesn’t “exhibit the lamentably low level nature of C.”

  • September 21, 2022 at 1:02 pm
    Permalink

    It demonstrates something fun and interesting you can do with C. As for “conforming”, I’m aware that this is undefined behavior according to the C standard even though it is obviously correct semantics for C and works in 3 widely used compilers. The concept of undefined behavior in the C standards is a serious error which has had the effect of both narrowing the effective domain of C programmers and making C semantics opaque. It’s not a useful concept.

Comments are closed.