Last update: 07-Nov-2021
Author: R. Koucha
System call overload with stack backtrace
Introduction

For debug purposes, when a program is getting so big that it is becoming out of control, it is useful to catch the stack backtrace of a call to a given service.
This paper presents a method for a C language program in GNU/GLIBC environment.

Test environment

Overload of a service

Hereunder is a source file (write_ovl.c) to overload a Linux system call wrapped into the C library (not all system calls are wrapped on all achitectures). Here we overload write(). The actual symbol address is retrieved thanks to dlsym() service, the function displays the call stack with backtrace() and backtrace_symbols() before calling the actual service.

#define _GNU_SOURCE
#include <sys/types.h>
#include <unistd.h>
#include <dlfcn.h>
#include <stdio.h>
#include <execinfo.h>
#include <stdlib.h>

typedef ssize_t (* orig_write_t)(int fd, const void *buf, size_t count);

static orig_write_t orig_write;


ssize_t write(int fd, const void *buf, size_t count) {

  void *stack_entries[50];
  int n, i;
  char **syms;

  // 1st call ?
  if (!orig_write) {
    orig_write = dlsym(RTLD_NEXT, "write"); 
    printf("Syscall 'write@%p' is overloaded\n", orig_write);
  }

  printf("\n=====> Write called with %d, %p, %zu:\n", fd, buf, count);

  // Get the callstack
  n = backtrace(stack_entries, 50);
  syms = backtrace_symbols(stack_entries, n);
  if (syms) {
    for (i = 0; i < n; i ++) {
      printf("%s\n", syms[i]);
    }
    free(syms);
  }
  printf("========================\n");

  // Call the actual system service
  return (*orig_write)(fd, buf, count);
}

The example main program (try.c) into which we want to spy the system call is:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>


#define STR "string\n"
#define STR2 "string2\n"



void service(int fd) {

   write(fd, STR2, strlen(STR2));

}


int main(void) {

  int fd;

  fd = open("/tmp/file", O_RDWR|O_CREAT, 0777);

  write(fd, STR, strlen(STR));
  write(fd, STR2, strlen(STR2));
  service(fd);

  close(fd);

  return 0;

}
Build

The overloading object is built as a shared library. The -rdynamic options instructs the linker to add all symbols, not only used ones, to the dynamic symbol table. This option is needed to obtain backtraces from within a program.

$ gcc try.c -o try -g -rdynamic
$ gcc write_ovl.c --shared -fPIC -o write_ovl.so -ldl
Execution

Execution without symbol overloading:

$ ./try
$ cat /tmp/file
string
string2

Execution with write overload (we use the LD_PRELOAD environment variable):

$ LD_PRELOAD=./write_ovl.so ./try
Syscall 'write@0x7fc582acc210' is overloaded


=====> Write called with 3, 0x557683b7b9e7, 7:
./write_ovl.so(write+0xaa) [0x7fc582dad884]
./try(main+0x3c) [0x557683b7b91a]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xe7) [0x7fc5829ddbf7]
./try(_start+0x2a) [0x557683b7b7da]
========================


=====> Write called with 3, 0x557683b7b9d4, 8:
./write_ovl.so(write+0xaa) [0x7fc582dad884]
./try(main+0x52) [0x557683b7b930]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xe7) [0x7fc5829ddbf7]
./try(_start+0x2a) [0x557683b7b7da]
========================


=====> Write called with 3, 0x557683b7b9d4, 8:
./write_ovl.so(write+0xaa) [0x7fc582dad884]
./try(service+0x21) [0x557683b7b8db]
./try(main+0x5c) [0x557683b7b93a]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xe7) [0x7fc5829ddbf7]
./try(_start+0x2a) [0x557683b7b7da]
========================
About the author

The author is an engineer in computer sciences located in France. He can be contacted here.