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.

/* 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).  The expression “&amp 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.  Turning on the so-called “optimizer” breaks this code because current  C “optimizers” can change the semantics of C code in often unpredictable ways without making any performance improvements.  This is permitted by the C Standard  for reasons that strike me as not well thought out, but let’s pass over this sad situation for the moment.

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 

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;
}

There is a famous fragment of code in early UNIX versions that uses the same stack  trick. It goes something like 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 identified 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,  so “resume”  returns to the condition test.  There is a legendary comment in the original code, “You are not expected to understand this”, 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.