4

I'm trying to understand the relationship between C language system calls API, syscall assembler instruction and the exception mechanism (interrupts) used to switch contexts between processes. There's a lot to study out on my own, so please bear with me.

Is my understanding correct that C language system calls are implemented by compiler as syscall's with respective code in assembly, which in turn, are implemented by OS as exceptions mechanism (interrupts)?

So the call to the write function in the following C code:

#include <unistd.h>

int main(void)
{
    write(2, "There was an error writing to standard out\n", 44);
    return 0;
}

Is compiled to assembly as a syscall instruction:

mov eax,4       ; system call number (sys_write)
syscall 

And the instruction, in turn, is implemented by OS as exceptions mechanism (interrupt)?

Max Koretskyi
  • 101,079
  • 60
  • 333
  • 488
  • 1
    Well, the exact syscall mechanism is OS and architecture dependent, but you are not wholly on the wrong track:) – Martin James Apr 25 '18 at 15:21
  • `write` is a C function call. It's not directly compiled to a `syscall`. That is inside the function itself. `syscall 0x80` is not valid you are mixing that up with `int 0x80` which is the old 32 bit way to invoke the kernel. That is an interrupt. `syscall` does not use interrupt mechanism. – Jester Apr 25 '18 at 15:22
  • @Jester, yeah, thanks, I got that confused. I updated the code. But I'm more interested in broad picture now, not particularities of assembly (if that makes sense). And yes, I meant that the function `write` is implemented with `syscall` in assembly. I guess that's what you're saying – Max Koretskyi Apr 25 '18 at 15:35
  • @AngularInDepth.com: ISTM that you are **asking** about some "particularities of assembly". – Rudy Velthuis Apr 26 '18 at 22:27

4 Answers4

6

TL;DR

The syscall instruction itself acts like a glorified jump, it's a hardware-supported way to efficiently and safely jump from unprivileged user-space into the kernel.
The syscall instruction jumps to a kernel entry-point that dispatches the call.

Before x86_64 two other mechanisms were used: the int instruction and the sysenter instruction.
They have different entry-points (still present today in 32-bit kernels, and 64-bit kernels that can run 32-bit user-space programs).
The former uses the x86 interrupt machinery and can be confused with the exceptions dispatching (that also uses the interrupt machinery).
However, exceptions are spurious events while int is used to generate a software interrupt, again, a glorified jump.


The C language doesn't concern itself with system calls, it relies on the C runtime to perform all the interactions with the environment of the future program.

The C runtime implements the above-mentioned interactions through an environment specific mechanism.
There could be various layers of software abstractions but in the end the OS APIs get called.

The term API is used to denote a contract, strictly speaking using an API doesn't require to invoke a piece of kernel code (the trend is to implement non-critical functions in userspace to limit the exploitable code), here we are only interested in the subset of the API that requires a privilege switch.

Under Linux, the kernel exposes a set of services accessible from userspace, these entry-points are called system calls.
Under Windows, the kernel services (that are accessed with the same mechanism of the Linux analogues) are considered private in the sense that they are not required to be stable across versions.
A set of DLL/EXE exported functions are used as entry-points instead (e.g. ntoskrnl.exe, hal.dll, kernel32.dll, user32.dll) that in turn use the kernel services through a (private) system call.
Note that under Linux, most system calls have a POSIX wrapper around it, so it's possible to use these wrappers, that are ordinary C functions, to invoke a system call.
The underlying ABI is different, so is for the error reporting; the wrapper translates between the two worlds.

The C runtime calls the OS APIs, in the case of Linux the system calls are used directly because they are public (in the sense that are stable across versions), while for Windows the usual DLLs, like kernel32.dll, are marked as dependencies and used.

We are reduced to the point where an user-mode program, being it part of the C runtime (Linux) or part of an API DLL (Windows), need to invoke a code in the kernel.

The x86 architecture historically offered different ways to do so, for example, a call gate.
Another way is through the int instruction, it has a few advantages:

  • It is what the BIOS and the DOS did in their times.
    In real-mode, using an int instructions is suitable because a vector number (e.g. 21h) is easier to remember than a far address (e.g. 0f000h:0fff0h).
  • It saves the flags.
  • It is easy to set up (setting up ISR is relatively easy).

With the modernization of the architecture this mechanism turned out to have a big disadvantage: it is slow. Before the introduction of the sysenter (note, sysenter not syscall) instruction there was no faster alternative (a call gate would be equally slow).

With the advent of the Pentium Pro/II[1] a new pair of instructions sysenter and sysexit were introduced to make system calls faster.
Linux started using them since the version 2.5 and are still used today on 32-bit systems I believe.
I won't explain the whole mechanism of the sysenter instruction and the companion VDSO necessary to use it, it is only needed to say that it was faster than the int mechanism (I can't find an article from Andy Glew where he says that sysenter turned out to be slow on Pentium III, I don't know how it performs nowadays).

With the advent of x86-64 the AMD response to sysenter, i.e. the syscall/sysret pair, began the de-facto way to switch from user-mode to kernel-mode.
This is due to the fact that sysenter is actually fast and very simple (it copies rip and rflags into rcx and r11 respectively, masks rflags and jump to an address set in IA32_LSTAR).

64-bit versions of both Linux and Windows use syscall.

To recap, control can be given to the kernel through three mechanism:

  • Software interrupts.
    This was int 80h for 32-bit Linux (pre 2.5) and int 2eh for 32-bit Windows.
  • Via sysenter.
    Used by 32-bit versions of Linux since 2.5.
  • Via syscall.
    Used by 64-bit versions of Linux and Windows.

Here is a nice page to put it in a better shape.

The C runtime is usually a static library, thus pre-compiled, that uses one of the three methods above.

The syscall instruction transfers control to a kernel entry-point (see entry_64.s) directly.
It is an instruction that just does so, it is not implemented by the OS, it is used by the OS.

The term exception is overloaded in CS, C++ has exceptions, so do Java and C#.
The OS can have a language agnostic exception trapping mechanism (under windows it was once called SEH, now has been rewritten).
The CPU also has exceptions.
I believe we are talking about the last meaning.

Exceptions are dispatched through interrupts, they are a kind of interrupt.
It goes unsaid that while exceptions are synchronous (they happen at specific, replayable points) they are "unwanted", they are exceptional, in the sense that programmers tend to avoid them and when they happen is due to either a bug, an unhandled corner case or a bad situation.
They, thus, are not used to transfer control to the kernel (they could).

Software interrupts (that are synchronous too) were used instead; the mechanism is almost exactly the same (exceptions can have a status code pushed on the kernel stack) but the semantic is different.
We never deferenced a null-pointer, accessed an unmapped page or similar to invoke a system call, we used the int instruction instead.

Community
  • 1
  • 1
Margaret Bloom
  • 41,768
  • 5
  • 78
  • 124
3

EDIT

Yes, the C application calls a C library function which buried in the C library solution is a system specific call or set of calls, which use an architecturally specific way to reach the operating system, which has an exception/interrupt handler setup to deal with these system calls. Actually doesnt have to be architecturally specific, can simply jump/call to a well known address, but with modern desire for security and protection modes, a simple call wont have those added features, still functionally correct though.

How the library is implemented is implementation defined. And how the compiler connects your code to that library runtime or link time has a number of combinations as to how that can happen, there is no one way it can or needs to happen, so it is implementation defined as well. So long as it is functionally correct and doesnt interfere with the C standards then it can work.

With operating systems like windows and linux and others on our phones and tables there is a strong desire to isolate the applications from the system so they cannot do damage in various ways, so protection is desired, and you need to have an architecturally specific way to make a function call into the operating system that is not a normal call as it switches modes. If the architecture has more than one way to do this then the operating system can choose one or more of the ways as part of their design.

A "software interrupt" is one common way as with hardware interrupts most solutions include a table of handler addresses, by extending that table and having some of the vectors be tied to a software created "interrupt" (hitting a special instruction rather than a signal changing state on an input) but go through the same stop, save some state, call the vector, etc.

old_timer
  • 69,149
  • 8
  • 89
  • 168
  • If you look at LC-3 it is called a TRAP there. In ARM the instruction used to be called SWI but is now SVC, same machine code and function, both mnemonics are supported at least by gnu assembler and I would assume others. The implementation is not exact across architectures but the overall functionality is. There are some architectures that there are specific instructions that do things an instruction to write to the uart vs it being mapped into an address space, way back when that kinda made sense, today you see this on cpus that are not going to run an OS necessarily. – old_timer Apr 25 '18 at 17:17
  • Thanks for your elaborate answer. May I suggest you start the answer with _Bottom line is yes you appear to understand this..._ and put the details after that? – Max Koretskyi Apr 25 '18 at 19:20
  • _One would expect that system call to be written in assembly language_ - and stored as object file so the linker can later pick it up, correct? – Max Koretskyi Apr 25 '18 at 19:24
  • You would expect that but sadly for various reasons it is not uncommon for folks to use inline assembly instead of real assembly. – old_timer Apr 25 '18 at 23:45
  • *Intel* didn't define software interrupts as the only system-call mechanism, that was Linux's choice. They could have picked call-gates with a `call far` to a specific descriptor, because Intel went nuts with features for privilege levels in 386 (See [OsDev syscall/sysret and sysenter/sysexit instructions enabling](//stackoverflow.com/q/46022184) for details on the options). Fun fact: on 386 the fastest way to enter the kernel was an illegal-instruction trap, and some OSes used that as their system-call mechanism. – Peter Cordes Apr 26 '18 at 02:49
  • (nice fun facts)I didnt mean to imply that quite that way. I meant to imply long before linux, long before windows, and in a way we still see today their path into "system" calls was a "software interrupt". But as you are pointing out, wasnt intel but we could say IBM which greatly helped the future of the 8086, chose to use those software interrupts for system calls rather than choosing another solution. (Microsoft, ditto, etc) Then as each new operating system was created or ported, into each current and new member of the family, opportunity to change was there, along with new choices – old_timer Apr 26 '18 at 11:50
3

Is my understanding correct that C language system calls are implemented by compiler as syscall's with respective code in assembly […]?

No.

The C compiler handles system calls the same way that it handles calls to any other function:

; write(2, "There was an error writing to standard out\n", 44);
mov    $44, %edx
lea    .LC0(%rip), %rsi  ; address of the string
mov    $2, %edi
call   write

The implementation of these functions in libc (your system's C library) will probably contain a syscall instruction, or whatever the equivalent is on your system's architecture.

  • Thanks for your input! Upvoted. It was a bad word choice on my part. I meant the `write` function itself is implemented as `syscall`. But, anyways, you confirmed my expectations – Max Koretskyi Apr 26 '18 at 05:10
2

Not a direct answer to the question but this might interest you (I don't have enough karma to comment) - it explains all the user space execution (including glibc and how it does syscalls) in detail:

http://www.maizure.org/projects/printf/index.html

You'll probably be interested in particular in 'Step 8 - Final string written to standard output':

And what does __libc_write look like...?

000000000040f9c0 <__libc_write>:
  40f9c0:  83 3d c5 bb 2a 00 00   cmpl   $0x0,0x2abbc5(%rip)  # 6bb58c <__libc_multiple_threads>
  40f9c7:  75 14                  jne    40f9dd <__write_nocancel+0x14>

000000000040f9c9 <__write_nocancel>:
  40f9c9: b8 01 00 00 00          mov    $0x1,%eax
  40f9ce: 0f 05                   syscall 
  ...cut...

Write simply checks the threading state and, assuming all is well, moves the write syscall number (1) in to EAX and enters the kernel.

Some notes:

  • x86-64 Linux write syscall is 1, old x86 was 4
  • rdi refers to stdout
  • rsi points to the string
  • rdx is the string size count

Note that this was for the author's x86-64 Linux system.

For x86, this provides some help:

http://www.tldp.org/LDP/khg/HyperNews/get/syscall/syscall86.html

Under Linux the execution of a system call is invoked by a maskable interrupt or exception class transfer, caused by the instruction int 0x80. We use vector 0x80 to transfer control to the kernel. This interrupt vector is initialized during system startup, along with other important vectors like the system clock vector.

But as a general answer for a Linux kernel:

Is my understanding correct that C language system calls are implemented by compiler as syscall's with respective code in assembly, which in turn, are implemented by OS as exceptions mechanism (interrupts)?

Yes