1. Introduction

When developing for or simply working with a Linux system, we constantly leverage system calls, at the very least, for handling devices. While not necessarily inevitable, library calls are usually also part of almost every task.

In this tutorial, we’ll explain system and library calls in Linux, showing how they differ. First, we explore system calls in depth. Next, we turn to library calls. Finally, we’ll show the links and differences between the two call types.

We tested the code in this tutorial on Debian 11 (Bullseye) with GNU Bash 5.1.4. It should work in most POSIX-compliant environments.

2. Linux System Calls

System calls are a way for software to switch to ring 0 and run basic operating system (OS) functions. Linux system calls (syscalls) provide access to many aspects of the kernel:

  • memory access
  • filesystem management
  • networking
  • process handling like creation, listing, and killing

For example, the Linux write system call can write to a file descriptor like stdout.

In the example code, we use gcc and inline Assembly with AT&T syntax.

2.1. System Interrupt Instruction

Although both are kernel communication mechanisms, system calls aren’t system interrupts.

Still, we can initiate a system call with the older int instruction and the interrupt number 0x80 (128):

int main(int argc, char *argv[]) {
  __asm__ (
    ".section .data;"
    "  1: .string \"Baeldung.\";"
    ".section .text;"
    "  movl $4, %%eax;"
    "  movl $1, %%ebx;"
    "  leal 1b, %%ecx;"
    "  movl $9, %%edx;"
    "  int $0x80"
    ::: "eax", "ebx", "ecx", "ebx", "memory"
  );

  return 0;
}

Let’s dissect this inline Assembly code within C. First, we declare a “Baeldung.” string. After that, we store the system call number (4, write) in the eax register. Next, ebx holds the file descriptor number (1, stdout) to write to.

After that, *we load the address (label 1, going [b]ackward) of our string into the ecx register, storing its length in edx*. Finally, we perform an interrupt via int $0x80.

We can now install some prerequisites, save the code above as int0x80-4.c, assemble it, and run the result to see the expected output:

$ apt-get install gcc-multilib
[...]
$ gcc -m32 -no-pie int0x80-4.c -o int0x80-4
$ ./int0x80-4
Baeldung.

As we can see, there are two main conditions for the use of int 0x80:

  • -m32 to compile a 32-bit binary, even on a 64-bit system, which may require installing gcc-multilib on 64-bit systems
  • -no-pie to disable the creation of a position-independent executable (PIE), thus preventing address space layout randomization (ASLR), which disables issues around our code related to section relocation

In fact, system interrupts with int are the legacy way of performing system calls on 32-bit systems using their lookup tables.

While int can also work on 64-bit, it’s slow and not native. Newer methods usually involve faster instructions and potentially different system call numbers.

2.2. System Call Instructions

Depending on the architecture, the syscall (AMD) and sysenter (Intel) instructions may be available in different modes on 32-bit and 64-bit systems. However, syscall works in the 64-bit mode of both Intel and AMD processors, so we use that in our examples:

int main(int argc, char *argv[]) {
  __asm__ (
    ".data;"
    "  1: .string \"Baeldung.\";"
    ".text;"
    "  movl $1, %%eax;"
    "  movl $1, %%edi;"
    "  leal 1b, %%esi;"
    "  movl $9, %%edx;"
    "  syscall"
    ::: "eax", "edi", "esi", "edx", "memory"
  );

  return 0;
}

Now, let’s save the code above as syscall-1.c, assemble it, and run the result:

$ gcc syscall-1.c -o syscall-1
$ ./syscall-1
Baeldung.

In this case, we use 64-bit code to run the write system call via the syscall instruction. Notably, the lookup table here is different as the call number in eax is 1, not 4. Also, esi and not ecx holds the string address, while edi instead of ebx is the file descriptor number.

Naturally, dealing with Assembly isn’t very convenient, so let’s see what other methods we can leverage.

2.3. GNU C System Call

Linux provides an interface for system calls via the GNU C (glibc) library or glibc alternatives:

#include <sys/syscall.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    syscall(SYS_write, 1, "Baeldung.", 9);
    return 0;
}

Here, we invoke the write system call by identifying it as SYS_write to the syscall() library function call. But what are library calls?

3. Linux Library Calls

While system calls end up entering the kernel at ring 0, library calls typically execute in user space, ring 3. Meanwhile, drivers are kind of a blend between user and kernel mode code.

For brevity and convenience, we discuss and use the GNU C standard library with the C language. However, other languages can use that same library along with their own custom ones. The concepts are similar in all cases.

3.1. Libraries

When developing, we usually aim to reuse standard time-tested code instead of rewriting it each time. Often, such code comes in the form of libraries as library files with exported functions:

$ ls -l *.so
-rwxr-xr-x 1 root root  177928 Oct 10 06:56 ld-2.31.so
-rw-r--r-- 1 root root   19048 Oct 10 06:56 libanl-2.31.so
lrwxrwxrwx 1 root root      33 Oct 10 06:56 libanl.so -> /lib/x86_64-linux-gnu/libanl.so.1
-rw-r--r-- 1 root root 1468832 Feb 20  2021 libbfd-2.35.2-system.so
-rw-r--r-- 1 root root   88136 Sep 21 06:40 libbind9-9.16.33-Debian.so
-rw-r--r-- 1 root root   14432 Oct 10 06:56 libBrokenLocale-2.31.so
lrwxrwxrwx 1 root root      42 Oct 10 06:56 libBrokenLocale.so -> /lib/x86_64-linux-gnu/libBrokenLocale.so.1
-rwxr-xr-x 1 root root 1905632 Oct 10 06:56 libc-2.31.so
lrwxrwxrwx 1 root root      16 Jun 22  2022 libcrypto.so -> libcrypto.so.1.1
lrwxrwxrwx 1 root root      35 Apr  4  2021 libcrypt.so -> /lib/x86_64-linux-gnu/libcrypt.so.1
-rw-r--r-- 1 root root     283 Oct 10 06:56 libc.so
-rw-r--r-- 1 root root 1831536 Feb  2  2021 libdb-5.3.so
[...]

Here, we see glibc as libc-2.31.so. To leverage its functions, we use static or dynamic linking to either include or point to the library we need, as long as it’s installed.

In essence, this means a library is user (space) code.

3.2. Check Library Functions

Since at least some form of glibc is part of most Linux distributions, gcc also comes prepackaged with header files defining each function.

Let’s see what our installed version of glibc exports via the -s switch of readelf:

$ readelf -s /usr/lib/x86_64-linux-gnu/libc-2.31.so
Symbol table '.dynsym' contains 2370 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
[...]
   639: 0000000000053cf0   200 FUNC    GLOBAL DEFAULT   13 printf@@GLIBC_2.2.5
[...]

Here, we see that printf is one of the many functions defined in the GNU C library. Let’s see how we call it.

3.3. Library Function Call

As long as we have the necessary header files to import what we need, we can simply write and compile C code with standard function calls since the compiler links to the standard C library by default:

$ cat libcall.c
#include <stdio.h>

int main(int argc, char *argv[]) {
    printf("Baeldung.");
    return 0;
}
$ gcc libcall.c -o libcall
$ ./libcall
Baeldung.

Here, we use printf() to output text to stdout. While printf() itself is a user-space function, it also performs the write system call we already discussed. This is just one of the links between the two types of calls.

4. System and Library Call Structure Overview

Modern OS implementations usually have hierarchies that look more or less similar to each other:

__________________
| applications | |________________ | | library calls | | |______________ | | | system calls | | | |____________ | | | | kernel | | | | |__________ | | | | | hardware | | | | | |__________|_|_|_|_|

Notably, library calls sit between system calls and applications. Thus, we usually don’t and shouldn’t need to use Assembly and directly invoke system call instructions.

Instead, GNU C doesn’t only provide functions like syscall() but also direct mappings like the write() function:

#include <sys/syscall.h>
#include 

int main(int argc, char *argv[]) {
    write(1, "Baeldung.", 9);
    return 0;
}

Obviously, write() is a wrapper, which just handles the kernel communication just like syscall(). So, we end up making library calls, which run system calls.

In fact, we can confirm this via strace:

$ cat int0x80-4.c
int main(int argc, char *argv[]) {
  __asm__ (
    ".section .data;"
    "  1: .string \"Baeldung.\";"
    ".section .text;"
    "  movl $4, %%eax;"
    "  movl $1, %%ebx;"
    "  leal 1b, %%ecx;"
    "  movl $9, %%edx;"
    "  int $0x80"
    ::: "eax", "ebx", "ecx", "ebx", "memory"
  );

  return 0;
}
$ gcc -m32 -no-pie int0x80-4.c -o int0x80-4
$ strace int0x80-4
[...]
write(1, "Baeldung.", 9Baeldung.)                = 9
exit_group(0)                           = ?
+++ exited with 0 +++
$ cat syscall-wrap.c
#include <sys/syscall.h>
#include 

int main(int argc, char *argv[]) {
  syscall(SYS_write, 1, "Baeldung.", 9);
  return 0;
}
$ gcc syscall-wrap.c -o syscall-wrap
$ strace syscall-wrap
[...]
write(1, "Baeldung.", 9Baeldung.)                = 9
exit_group(0)                           = ?
+++ exited with 0 +++

Here, we see that both our earlier examples end up using the write system call, as seen in the output of strace for our process.

5. Summary

In this article, we explored system calls, library calls, and how they differ.

In conclusion, while the slower and non-portable system calls directly involve the kernel, the less-privileged but faster library calls can stay in user space unless an elevated operation is required.