Here I'm trying to understand what happens when I run
./hello
#include <stdio.h>
int main() {
printf("Hello!\n");
}
a simple "Hello World" program written in C, in Unix -- what I'd have to do if I wanted to write an OS that could execute it.
I'm going to assume that ./hello
is statically linked, because that sounds simpler to deal with. It's worth noting that a statically linked hello
is 868K on my machine. Eep.
I compiled it using
gcc -static hello.c -o hello
Any (nice!) comments or clarifications are appreciated.
To run a program, I have to be able to find the program. So there would need to be some kind of filesystem and I would need to read the file from somewhere.
In a Unix system, executables are in the ELF format.
So I would need to copy the "text" of the program somewhere.
There is a string in the program. It needs to go somewhere.
This program doesn't actually allocate memory, so perhaps it does not need a heap and it doesn't matter where the heap pointer is. It does need a stack. stack overflow question on how the stack works in assembly
hello
has some system calls in it. I found this out by running
objdump -d -M intel hello | grep 'syscall'
syscall
is an assembly instruction for making a system call. That looks like
401385: b8 03 00 00 00 mov eax,0x3
40138a: 0f 05 syscall
The number stored in eax
is the system call that is called. In this case, 3
There are 119 instances of syscall
, and it's using several different system calls. This is worrying.
(Explained more in this stackoverflow question)
I have no idea how the OS would check up on the program. I guess it doesn't just let the program run, but takes away control periodically and makes sure the stack pointer hasn't moved too far. How would it take away control? Hmm.
When there is a stack overflow I guess it sends a signal to the program, which is a POSIX thing.
I do not understand this.
There are no malloc
s in the program, so I would not need to allocate memory for it or anything.
What else?!??
- How long would this take for a human (where human = me) to write from scratch?
- Is there a way to write a smaller program with less system calls and magic? There are like 50 system calls and what are they even doing?
- Do I need a heap if I never use
malloc
? - Could I write my own printf in assembly that does less and is simpler? Just printing a string is pretty easy...
- How do I kill a program?
I don't think you should list "copy the data somewhere" as a separate step. By the time the linker has finished creating the executable program, the static string "Hello!\n" has been embedded into the executable, and the linker has calculated where it is and arranged that the parts of the program that need it are passed a pointer to the actual data. (If you run the
strings
utility on the executable file you should see the string hiding in there.) So the data is embedded in the program text and needn't be copied separately.The statically linked
hello
executable has to include all the code forprintf
and for the C standard I/O library, which is surprisingly complex underneath.One thing I would have included that you omitted is the system calls. At some point
printf
has to tell the OS that it actually wants to write data somewhere. At the library level, the way this works is that somewhere down in the guts of the standard I/O library (which is now part of your program, remember) there is an invocation of the Unix "system call"write
. Typically the way this is implemented at the object level is that the program loads a magic number into one register, identifying which system call it wants to make (on my system I think the magic number is 4, and is listed in/usr/include/x86_64-linux-gnu/asm/unistd_32.h
.), and the arguments ofwrite
into other registers (or perhaps it pushes them on the stack; it varies from architecture to architecture). The arguments in this case are a pointer to the data to be written, a length, in this case 7, and a "file descriptor", which in this case is 1, representing standard output by convention.Then it executes a special machine instruction that causes a context switch to the kernel. The kernel has a
write
function in it that gathers the arguments and checks them for validity. For example, it would be bad if you could write a copy of some other process's memory onto the disk just by passing thewrite
function a pointer to it. The kernel checks the arguments and if they're valid it copies the 7 bytes somewhere. It stores the result of the write operation into the process structure; the C .library will take care of arranging that this result appears to have been returned from thewrite
function when the process resumes. Then the kernel context-switches back, or perhaps runs a completely different process.What happens to the 7 bytes inside the
write
call is very interesting and depends on where the process's standard output is pointing. This is recorded in the process structure in the process's open file table. The open file table maps file descriptors to "file pointers"; the file pointer in turn records an offset (maybe) and a pointer into the kernel's open file table. The open file table in turn will record whether the descriptor is attached to a network socket, a disk file, a pipe, or a device, and relevant details about each.In the most common case standard output is attached to a terminal device. In this case the open file table has two numbers. The "major device number" says that the device is a terminal, and indexes a table of pointers inside the kernel that point to the device driver routines that have the code for opening, closing, reading from, and writing to the terminal. The "minor device number" says which of the many otherwise identical terminal devices is being used, and is usually just passed to the device driver routines as a parameter.
The terminal device in this case isn't a physical terminal, which would require sending the data out some physical interface; it's what's called a "pseudo-tty". A pseudo-tty has two ends: a "slave" end that behaves like a real terminal, and a "master" end that can be used to control the terminal, read what was written to it, and send a reply. Data written to the slave side of a pseudo-tty is saved inside a kernel buffer until another process that has the master side open chooses to read it. Then that other process gets the seven bytes of data. In this case that process is your
xterm
terminal program. It then figures out what that should look like on its window and sends a bunch of requests to the X server via a network socket to tell it what to paint on the screen.If the
hello
program's standard output has been directed to a file instead of to the pseudo-tty, the disposition of the seven bytes is somewhat different. The kernel's open file table will record the identity of the disk device on which the file resides, and the "i-number" of the file on that device. (Every file has an i-number, and each file on a device has a different one.) The "i-number" is so called because it is the "index number" of the file in the disk's index; the first data on the disk is a giant table of "inodes" ("index nodes") which records the file's owner, its permissions, and (most important) where on the disk its data is located. The kernel has previously located the inode on disk and loaded it into memory; this is what happens when you open a file. It knows the offset into the file that at which the write should take place. So it can calculate which disk block needs to be modified. Using the device number of the device on which the file resides, it calls the device driver to read the correct block from the disk; it then modifies the right seven bytes, and it leaves the block in a kernel "dirty buffer" area of blocks that have been modified but not yet written to disk. At some point in the not-too-far future, it will call the device driver again to write the modified data back to the right place on the disk. If the system crashes before that happens, the write is lost.I hope this was not too much detail, and was what you were looking for.