5
$\begingroup$

I have an idea of how a C program is turned into machine code by the compiler. I also know how the processor processes the instructions (https://www.youtube.com/watch?v=cNN_tTXABUA this video has a good introduction). But what I don't understand, is how an operating system (most times written in C or some low-level language) can run programs also in C (or other low-level language). I don't understand this. Does the OS read the code and then processes it with some internal functions, or does it only open the machine code and send it to the processor, that makes the rest? In case of the second option, how the OS take care of which instructions are allowed to be executed, and which are not? (example: I may write a program that has an instruction that jumps to a forbidden part of the memory RAM, how the OS protect it from happening?)

I don't expect to understand it fully in this post's answers, but if you guys could give me an idea and then some books or tags to search, I'd be happy!

$\endgroup$
  • $\begingroup$ Skipping details: it loads the code in memory and jumps to its beginning address. The computer then executes starting from that address. The OS provides services the program can call. It can also interrupt the program if need be (specific hardware for that). $\endgroup$ – babou Apr 6 '14 at 1:11
  • $\begingroup$ @babou How can an instruction make a system call? How the OS handles it? I need to understand this in the physical layer. $\endgroup$ – Mr Janiqua Apr 6 '14 at 1:33
  • 1
    $\begingroup$ @MrJaniqua You are asking many questions and clearly we are not able to answer all of them here. I suggest you read a book on the subject. Unfortunately I can't recommend any, but perhaps you could ask a new question detailing exactly what kind of book you're interested in. $\endgroup$ – Yuval Filmus Apr 6 '14 at 2:27
  • $\begingroup$ @YuvalFilmus Tanenbaum's Modern Operating Sytems is a fairly common choice. $\endgroup$ – David Richerby Apr 6 '14 at 7:44
  • 1
    $\begingroup$ @MrJaniq Did you try searching the web with keywords like: operating systems introduction? That is the first thing to do. It is true that what you find may not give the most elementary details, which you received here. But learning is done mainly by searching existing information and asking only for what you cannot find. We all have limited time, and that is why we write documents: they factor the use of our time between all the readers, if they will use part of their own time to find the documents that were written to explain things. And try to imagine answers to your own questions. $\endgroup$ – babou Apr 6 '14 at 16:23
14
$\begingroup$

It's hard to give a full answer to this question, as it would basically amount to an introductory text in operating system design, so I will try to give some pointers.

First of all, if you really want to know (including all the gritty details) how an operating system actually works, there's probably no way around looking at an actual implementation of one. Luckily, there's xv6 providing quite a good example, see xv6 and follow the references there.

Before diving into the code, it's probably a good idea to have a look at the basic components of an operating system, see Operating System Components.

I think your question has three main aspects

  1. How does an operating system load a (compiled) C program?
  2. How does such a program interact with the system it runs on?
  3. How does the operating system prevent bad things from happening and regain control once the program is running?

(Warning: The following might be oversimplifying things)

For the first question, see Loader. A compiled C program is just binary data. When loading (let us ignore some bookkeeping that is also going on around this), the operating system copies this binary data simply into memory, does some initialisation (e.g. copy arguments at places the program can find it) and then sets the instruction pointer to the start of the loaded program. Basically, from this point on the program is in control (see below). Eventually, the program (hopefully) will issue a system call, namely exit, when it is done, thus handing control back to the operating system, see Exit (system call (it might also terminate by using a simple return in main, but let us ignore that case for the moment).

This leads to the second question. The interaction with the operating system takes place via system calls. These, in turns, are implemented via interrupts. Roughly speaking, you can think of a special processor instruction being called (i.e., it's a feature of the processor), which stops the execution of the current code and starts the execution of an interrupt handler, which is part of the operating system. The interrupt handler than tries to figure out why it was called and will act accordingly. How does the processor know where to find the interrupt handler? This is part of the loading process of the operating system at boot time, see e.g. Kernel startup stage.

For the third question, see Process management:

There are two possible ways for an OS to regain control of the processor during a program’s execution in order for the OS to perform de-allocation or allocation:

  1. The process issues a system call (sometimes called a software interrupt); for example, an I/O request occurs requesting to access a file on hard disk.
  2. A hardware interrupt occurs; for example, a key was pressed on the keyboard, or a timer runs out (used in pre-emptive multitasking).

The situation is slightly different on multi-core architectures, as there obviously the operating system and the loaded program can run concurrently.

For the memory related question, see Memory protection. Again, the short answer is that this is supported by processor features, i.e., the operating system can set boundaries when loading a program leading to an interrupt when these boundaries are violated. Another important feature related to this are CPU modes.

To sum things up, in order to understand operating systems, it's also important to understand computer architecture, as many features of an operating system are based in corresponding features of processors. I always found the following a good introductory textbook: "Computer Organization and Design: The Hardware/Software Interface" by David Patterson and John Hennessy.

Finally, the following (Linux-specific) example might help. The usual

#include <stdlib.h>
#include <stdio.h>

int main(void) {
    printf("Hello, world!");
    exit(0); 
}

will turn into assembler code as follows (more or less..., stolen from the Wikipedia article on Netwide Assembler)

; compile on 64-bit Linux using
; nasm -f elf syscall.asm
; ld -m elf_i386 -s -o syscall syscall.o

global _start

section .text
_start:
    mov eax, 4 ; system call number 4: write
    mov ebx, 1 ; file descriptor 1: stdout
    mov ecx, msg
    mov edx, msg.len
    int  0x80  ; Passes control to interrupt vector
                   ; invokes system call, in this case system call
                   ; write(stdout, msg, strlen(msg));

    mov eax, 1 ; system call number 1: exit()
    mov ebx, 0 ; exit status 0
    int 0x80   ; Passes control to interrupt vector
               ; invokes system call, in this case system call
               ; number 1 with argument 0, i.e., exit(0)

section .data
msg:    db  "Hello, world!", 10
.len:   equ $ - msg

Once actually compiled, this will look like (just the part between the first and last column, the rest is for illustration purposes, generated using xxd programName)

0000000: 7f45 4c46 0101 0100 0000 0000 0000 0000  .ELF............
0000010: 0200 0300 0100 0000 8080 0408 3400 0000  ............4...
0000020: cc00 0000 0000 0000 3400 2000 0200 2800  ........4. ...(.
0000030: 0400 0300 0100 0000 0000 0000 0080 0408  ................
0000040: 0080 0408 a200 0000 a200 0000 0500 0000  ................
0000050: 0010 0000 0100 0000 a400 0000 a490 0408  ................
0000060: a490 0408 0e00 0000 0e00 0000 0600 0000  ................
0000070: 0010 0000 0000 0000 0000 0000 0000 0000  ................
0000080: b804 0000 00bb 0100 0000 b9a4 9004 08ba  ................
0000090: 0e00 0000 cd80 b801 0000 00bb 0000 0000  ................
00000a0: cd80 0000 4865 6c6c 6f2c 2077 6f72 6c64  ....Hello, world
00000b0: 210a 002e 7368 7374 7274 6162 002e 7465  !...shstrtab..te
00000c0: 7874 002e 6461 7461 0000 0000 0000 0000  xt..data........
00000d0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000e0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000f0: 0000 0000 0b00 0000 0100 0000 0600 0000  ................
0000100: 8080 0408 8000 0000 2200 0000 0000 0000  ........".......
0000110: 0000 0000 1000 0000 0000 0000 1100 0000  ................
0000120: 0100 0000 0300 0000 a490 0408 a400 0000  ................
0000130: 0e00 0000 0000 0000 0000 0000 0400 0000  ................
0000140: 0000 0000 0100 0000 0300 0000 0000 0000  ................
0000150: 0000 0000 b200 0000 1700 0000 0000 0000  ................
0000160: 0000 0000 0100 0000 0000 0000            ............

This is the thing (or rather, a binary representation thereof) the operating system loads into memory. We might also want to have a look at it with objdump -s -d programName which gives some more information

Contents of section .text:
 8048080 b8040000 00bb0100 0000b9a4 900408ba  ................
 8048090 0e000000 cd80b801 000000bb 00000000  ................
 80480a0 cd80                                 ..              
Contents of section .data:
 80490a4 48656c6c 6f2c2077 6f726c64 210a      Hello, world!.  

Disassembly of section .text:

08048080 <.text>:
8048080: b8 04 00 00 00       mov    $0x4,%eax
    8048085: bb 01 00 00 00       mov    $0x1,%ebx
804808a: b9 a4 90 04 08       mov    $0x80490a4,%ecx 
    804808f: ba 0e 00 00 00       mov    $0xe,%edx
8048094: cd 80                int    $0x80
    8048096: b8 01 00 00 00       mov    $0x1,%eax
804809b: bb 00 00 00 00       mov    $0x0,%ebx
    80480a0: cd 80                int    $0x80

You see that this program has two system calls. The first one is write which outputs the "Hello, world!", the second one is the exit. These two system calls correspond to the int 0x80 in the assembler code that generates a software interrupt, see INT instruction and http://www.linfo.org/int_0x80.html Before making a system call via int 0x80 we need to tell the Linux, which system call we want (plus set up any other relevant parameters). System calls are identified by numbers, e.g. 1 is exit, 4 is write, see http://syscalls.kernelgrok.com/

Again, to sum this part up: System calls are translated into int 0x80 instructions (plus necessary parameter handling before that). Once the program runs and hits an int 0x80 instruction, the processor will give control to the operating systems interrupt handler which sees that it has to handle a system call and then does things accordingly.

$\endgroup$
6
$\begingroup$

When you compile a C program, the program is turned into an executable. You can think of an executable basically as raw machine code — the real picture is more complicated, but you can ignore these details for now. When you run a program from the command line (say), what the operating systems does is it loads (i.e., copies) the machine code found in the executable into memory, and then transfers control (jumps) there. In Unix, this is accomplished by the system call execve. The loading process is more complicated, since the operating system has to set up the environment and load dynamic libraries, but you can ignore these details for now.

If the code jumps into a forbidden location, then an interrupt is triggered. An interrupt is basically a jump to some location determined earlier by the operating system (in modern CPUs, only the operating system can modify these addresses). The operating system then triggers a signal (SIGSEGV or Segmentation fault) which eventually leads to a core dump (unless it is handled otherwise by the thread calling execve). The interpretation of forbidden location depends on the CPU and the operating system — indeed, buffer overflows take advantage of weaknesses in this mechanism.

Some programs are run on a virtual machine, for example Java and C#. From the point of view of the operating system, it all looks the same (unless the interpreter of the virtual machine is part of the OS itself), but the code itself is run on an imaginary CPU simulated by the interpreter in one way or another (see JIT compilation for the latest word on the subject). The interpreter can in principle ensure that the program doesn't do anything crazy.

Finally, regarding the title of your question: this has nothing to do with the C library. The loader is part of the operating system, not of the standard C library libc. An operating system usually provides a standard C library, but there are several other options, and you can choose for example to use GNU's glibc over the native standard C library (if it is different).

$\endgroup$
  • $\begingroup$ But does the program run by itself, or the OS executes its instructions? I can't picture how a program can make system calls, if it is a machine code running in the processor. Also, how can the OS schedule these processes? The books only give me abstraction, but I want to understand it in the physical layer. Can you give me some path to follow and understand what you understand? What you did to know what you know today? Thank you so much, I'm intersted in learning. $\endgroup$ – Mr Janiqua Apr 6 '14 at 1:32
  • $\begingroup$ The CPU runs the program – indeed eventually the CPU runs everything. If the code is interpreted or runs on a virtual machine, then there is an intermediary, but eventually everything runs on the CPU. $\endgroup$ – Yuval Filmus Apr 6 '14 at 2:23
  • $\begingroup$ Scheduling works through time interrupts – every so often the operating system regains control and can decide which process runs next. Matters are probably more complicated on today's multicore architecture, in which several cores are running head-to-head and need to be synchronized. $\endgroup$ – Yuval Filmus Apr 6 '14 at 2:23
  • $\begingroup$ System calls are traditionally implemented by the process issuing a software interrupt. The operating system gains control and executes the system call. Some modern CPUs have other mechanisms, check for example the Wikipedia page. $\endgroup$ – Yuval Filmus Apr 6 '14 at 2:26
  • 1
    $\begingroup$ The operating system regains control through the timer interrupt. Every so often a hardware device in your computer triggers an interrupt, and lets the OS regain control. Check for example here: stackoverflow.com/questions/14481032/…. $\endgroup$ – Yuval Filmus Apr 6 '14 at 3:17
1
$\begingroup$

A very practical way to look at this is from the point of view of the shell you'd use after you log in to Linux. The shell itself will probably be written in C. The shell will call fork() to divide itself into two processes, and then call exec() from the second of those processes to replace the second process's code with that of the program you asked the shell to run. The shell's calls to fork() or exec() would go to functions in the C runtime library. The C runtime library provides all the functions specified as library functions in the C specification and/or POSIX specification. In general, fork(), exec() and all the other C library functions that rely on the OS will ultimately call syscall() which transitions into the kernel. The kernel has its own memory layout but can carefully access memory on behalf of a user process. The parameters to syscall() specify which OS function should be run and are a version of the parameters passed to the C runtime function. The syscall() piece is optimized in some Linux implementations but in others it is a software interrupt or trap, and the optimization implemented in for example x86 architecture is pretty much just a speedier way of doing the exact same thing. This interrupt or trap causes the kernel to execute from some fixed address within the kernel code, which translates the parameters that syscall was passed into a call to the appropriate C function in the kernel itself. If the parameters to the C runtime library function involve pointers to strings or buffers, those are passed across the syscall call, and then the function in the kernel that implements the OS function will probably need to obtain versions of those pointers that are valid in the context of the kernel.

Your question also asks about memory access protection. Very generally speaking, a user process or the kernel itself have access to defined regions of memory, which are described in tables that the kernel programs into the CPU. A table entry in the CPU will specify a range of memory in terms of virtual addresses, the same range as physical addresses, and whether that should be accessible to the user program or just to the kernel. This table is known as the GDT on the x86 architecture. Additional features of such an entry could include whether reading, writing or executing are allowed for that chunk of memory. But when the CPU is running the user process, the only valid memory accesses are the ones that are specified for the user process. You have multiple user processes running, and to simplify, when the kernel switches from running one user process to running another, it switches out the old user process memory map and switches in a new user process memory map, so that the processes don't have a way to see each other's memory. If a process tries to violate the rules of the memory map, it results in running a trap handler in the kernel.

Within the memory mapping system I described there are optimizations. One allows some sharing of the code when programs are linked to the same dynamic shared object (libc.so being an important example of one that it pays well to share) and for optimizing other things.

Some of the interface between the CPU itself and the kernel involves special functions executing. These functions can be written in C as long as they are tagged with special attributes to cause the C compiler to generate the entry code of the function in a way that is compatible with how the CPU will call the function. GCC supports the attribute "interrupt" as one way to code such functions but the details depend on the architecture.

$\endgroup$
0
$\begingroup$

I think what is being asked is more of a chicken vs. egg problem. If an operating system is written in C, but you have to a compiler/compiled version of the operating system to run a C program, how do you even run the OS in the first place??? The short answer is a bootstrap program. When you turn on the computer hardware will run some tests, then proceed to run a small program saved in ROM/BIOS/Firmware. This in turn gets the OS up and running. This is my general understanding anyway. https://www.cs.rutgers.edu/~pxk/416/notes/02-boot.html This article helped me wrap my brain around the concept.

$\endgroup$
  • 1
    $\begingroup$ I'm not sure this really answer the question. The key point is simply that the low-level OS doesn't care what language a program was written in. $\endgroup$ – David Richerby Jan 23 '18 at 22:00

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.