Getting Rid of Stdlibc
tags: assembling, compiling, linking
To get started writing your own implementation of the standard C library you'll need to stop linking against whatever implementation that your compiler comes with. I'm working on a CentOS 7 machine and use glibc normally; now I want to use welibc (even though it doesn't really exist yet).
Normal Compilation
First, we're going to look at a "normal" compile. The beginning of the test suite for welibc is in a file called main_test.c and looks like this (comment blocks excluded)
int
main(int argc, char *argv[])
{
int ret = 0;
return ret;
}
Easy enough. This should just run and return 0 without doing anything else.
I'm using gcc
to compile this project, so to compile this code we can use the
following command
$ gcc -o test_welibc main_test.c
If things went properly then this compiles with no errors and gcc
doesn't
print anything. I'm also using bash for my shell and we can test that it ran
properly like so
$ ./test_welibc
$ echo $?
0
The echo $?
line will print the exit status of the most recently executed
foreground pipeline, e.g. the return value of the program we just ran. We got 0
which means things are working properly. You can be sure by using a much
different return value
int
main(int argc, char *argv[])
{
int ret = 42;
return ret;
}
Then rerun it
$ gcc -o test_welibc main_test.c
$ ./test_welibc
$ echo $?
42
So, now you know how a normal compilation would run.
Compiling without stdlibc
We never explicitly told gcc
to use glibc but it does this automatically
unless you instruct it not to. You would do that with the -nostdlib
-ffreestanding
flags, which prevent linking against stdlib and instructs gcc
that our final product will be running in a freestanding environment which means
the standard library might not exist and our program might not necessarily start
in the main
function. So, we'll add those to our compile line and see what
happens.
$ gcc -nostdlib -ffreestanding -o test_welibc main_test.c
/usr/bin/ld: warning: cannot find entry symbol _start; defaulting to 0000000000400144
Uh... what? /usr/bin/ld? entry symbol _start? First, we didn't even tell you to
run ld
and second, my code doesn't even mention _start, so why do you need that
symbol?
Compilation process
When you compile a C program, a couple things happen:
- The preprocessor performs text replacements
- The compiler converts C code into assembly instructions
- The assembler converts assembly instructions into into object code
- The linker combines all of the object files together into an executable
With the GNU compiler collection your preprocessor and compiler will be gcc
,
the assembler is as
, and the linker is ld
. gcc
will automatically run each
of these as instructed so you don't have to worry about doing each of them
individually. However, when you need the control, you can absolutely run each
step individually.
The output of each of these steps can be seen by using the -save-temps
flag to
gcc
, so we'll compile with that and look at each step
$ gcc -save-temps -nostdlib -ffreestanding -o test_welibc main_test.c
/usr/bin/ld: warning: cannot find entry symbol _start; defaulting to 0000000000400144
$ ls
main_test.c main_test.i main_test.o main_test.s test_welibc
Preprocessor
The preprocessor, gcc
in this case, moves through your C code and performs all
of the actions specified by the preprocessor directives, like:
#include
#define
#endif
This mostly performs text replacements within your code. For example, #include
actually inserts the text from whatever file you're including, and #define
replaces every instance of your macro with the value you assigned to it. It also
replaces every comment with a space.
The temporary file corresponding to this step is main_test.i
, let's look at
it:
# 1 "main_test.c"
# 1 "<command-line>"
# 1 "main_test.c"
# 47 "main_test.c"
int
main(int argc, char *argv[])
{
int ret = 42;
return ret;
}
There isn't really anything too interesting here, our code is mostly unchanged.
The .i file type indicates to gcc
that the code should not be preprocessed;
this makes sense because this is the output of the preprocessor. The first four
lines are all specific to the gcc
C preprocessor and contain a line number
followed by a filename in quotes. These aren't really important for now, but you
can read more about them here.
Compiler
The compiler, gcc
, will convert human readable code into assembly instructions
which correspond directly to a specific action that your processor needs to
carry out. For example, the processor doesn't know what return ret;
means, so
we need to translate that C code into the instructions that make the processor
"return the value of ret to the calling function." To look at the assembly,
check out the test_main.s file
.file "main_test.c"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl %edi, -20(%rbp)
movq %rsi, -32(%rbp)
movl $42, -4(%rbp)
movl -4(%rbp), %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (GNU) 4.8.2 20140120 (Red Hat 4.8.2-16)"
.section .note.GNU-stack,"",@progbits
This includes some things that are unique to the DWARF-2 debugging format and are too specific to get into right now. More information about these can be found here and in the DWARF-2 specification. The .ident and .section lines can also be ignored for the purposes of this article, but more info is here.
I'll add some comments to the assembly to indicate what it's doing
.file "main_test.c" # The file that these instruction were generated from
.text # The following instructions should be executable
.globl main # Declare a global symbol named main
.type main, @function # Set the "main" symbol to be of function type
main: # Define a label named main
.LFB0: # Define a label named .LFB0
.cfi_startproc
pushq %rbp # Push the value in the rbp register on to the stack
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp # Copy the contents of the rsp register into the rbp register
.cfi_def_cfa_register 6
movl %edi, -20(%rbp) # Move the contents of edi into memory
movq %rsi, -32(%rbp) # Move the contents of rsi into memory
movl $42, -4(%rbp) # Place the value 42 in memory
movl -4(%rbp), %eax # Move the value we stored in memory into the eax register
popq %rbp # Pop the value on top of the stack into the rbp register
.cfi_def_cfa 7, 8
ret # Return to the calling function
.cfi_endproc
.LFE0: # Define a label named .LFE0
.size main, .-main # Calculate the size of the main function
.ident "GCC: (GNU) 4.8.2 20140120 (Red Hat 4.8.2-16)"
.section .note.GNU-stack,"",@progbits
There is a lot going on here but the point is that you can see the 42 getting placed into memory, getting retrieved from memory and placed into a register, and finally the function returns to the caller. The eax register is the place where calling functions check for the return value.
Assembler
The assembler, /usr/bin/as
in this case, takes the assembly instructions in
the .s file and translates them into an object file which contains code that is
relocatable. This means that is will use offsets to reference things, rather
than hardcoded addresses, and will use a special address (like 0x00000000) to
indicate when something is unknown and needs to be fixed. This is done because
the assembler isn't aware of where this code will be placed in memory. The
special addresses will be fixed in the next stage. The object file also contains
the symbol table which correlates named objects to specific addresses, like
where the main
function is. There are several object file formats, the one we
are looking at is called ELF, executable and linkable format.
To view the information about the object file, use readelf
$ readelf -a main_test.o
This is too much information to look at, but I'll mention the symbol table specifically, which can be produced by itself with
$ readelf -s main_test.o
Symbol table '.symtab' contains 9 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS main_test.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 2
4: 0000000000000000 0 SECTION LOCAL DEFAULT 3
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 0 SECTION LOCAL DEFAULT 6
7: 0000000000000000 0 SECTION LOCAL DEFAULT 4
8: 0000000000000000 23 FUNC GLOBAL DEFAULT 1 main
Entry 8 is the entry for our main function, you can see the size of the function, the fact that it's a global function, and that it's value is 0 which means it hasn't been fixed up yet.
Linker
The linker, /usr/bin/ld
in this case, will take one or more object files and
combine them together by looking at their symbol tables to resolve unknown
symbols so that it knows exactly where to go when it needs to execute specific
parts of your code. The output of this stage is (hopefully) a fully working and
executable binary. We instructed gcc
to output a file named test_welibc
which
is the file that the linker produces. We can look at the final symbol table
associated with the executable to see how our symbols were resolved
$ readelf -s test_welibc
Symbol table '.symtab' contains 13 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000400120 0 SECTION LOCAL DEFAULT 1
2: 0000000000400144 0 SECTION LOCAL DEFAULT 2
3: 000000000040015c 0 SECTION LOCAL DEFAULT 3
4: 0000000000400170 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 0 FILE LOCAL DEFAULT ABS main_test.c
7: 0000000000000000 0 FILE LOCAL DEFAULT ABS
8: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _start
9: 0000000000601000 0 NOTYPE GLOBAL DEFAULT 4 __bss_start
10: 0000000000400144 23 FUNC GLOBAL DEFAULT 2 main
11: 0000000000601000 0 NOTYPE GLOBAL DEFAULT 4 _edata
12: 0000000000601000 0 NOTYPE GLOBAL DEFAULT 4 _end
Entry 10 is the entry for our main function and you can see that its value is no
longer 0. However, we also have 5 more symbols than before, including the _start
symbol that was said to be missing; where did they come from? These extra
symbols are needed by the ELF file format and gcc
creates them as part of the
linking process. However, the _start symbol is special because that is the
location where an ELF file will start executing. The standard C library provides
its own _start symbol and since we aren't linking against it anymore (because we
specified -nostdlib
) then we get the error about not finding a symbol. You'll
also see that the "Ndx" column lists the _start symbol as being UND, or
undefined.
Bootstrapping your library
Now that we know a little more about the compilation process, ELF, and why we ended up with an error, we can look at fixing it.
First, let's just run our executable and see what happens
$ ./test_welibc
Segmentation fault
A segmentation fault generally means that we accessed a region of memory that was protected in some way. It makes sense that we get this error because the ELF file format begins execution at the address associated with the _start symbol, and our _start symbol's address (the value from the symbol table) is 0. Most systems will not allow you to reference data at, or execute code at, the address 0; so, we end up with a seg fault.
This means we need to provide a _start symbol and somehow get it to call our main function. This will require us to do some assembly programming.
Defining the _start symbol
The first file in our library will be _start.S
, where we will define our start
symbol, call our main
function, and finally exit from the program.
To define a global symbol, we use the .globl
assembly directive, like so
.globl _start
This directive is specific to the GNU assembler (gas) so if you're using something else, be sure to look at its directive syntax (for example, nasm uses "global").
In assembly, to call a function we use the call
instruction, like so
call main
And to exit from the program we will make use of the exit(2) syscall through use
of the syscall
instruction
syscall
However, we must determine how to pass arguments to main, receives its return value, and pass an argument to the exit syscall. These calling conventions are established by the ABI for your architecture and operating system. In the case of Linux on x86_64 we can turn to the System V ABI which has this to say
Figure 3.4: Register Usage
%rax temporary register; with variable arguments
passes information about the number of vector
registers used; 1st return register
A.2.1 Calling Convention
1. User-level applications use as integer registers for passing the sequence
%rdi, %rsi, %rdx, %rcx, %r8 and %r9. The kernel interface uses %rdi,
%rsi, %rdx, %r10, %r8 and %r9.
2. A system-call is done via the syscall instruction. The kernel destroys
registers %rcx and %r11.
3. The number of the syscall has to be passed in register %rax.
Now that we know how to pass arguments, we will want to call main and exit like so
exit(main(argc, argv));
Where argc
and argv
are passed to main
, whose return value is passed to
exit
. Before calling main
we'll need to place argc
into %rdi
and argv
into %rsi
, main
will give its return value to us in %rax
which needs to be
placed into %rdi
, and finally we need to place the syscall number for exit
into %rax
before issuing the syscall
instruction. This leaves us with two
questions: where do argc
and argv
come from, and what is the syscall number
for exit
?
argc and argv
The ABI tells us where these come from in Figure 3.11: Initial Process Stack
Argument pointers 8 + %rsp
Argument count %rsp
And additionally makes this note about %rsp
's initial value
%rsp The stack pointer holds the address of the byte with lowest address
which is part of the stack. It is guaranteed to be 16-byte aligned
at process entry
Alignment will be important in just a moment. Additionally, the pointers in the
argv
array are located directly on the stack at the location specified above.
At the beginning of _start
, %rsp
will point to argc
and argv
will be 8
bytes higher on the stack. We can use pop
to grab the value at the location to
which %rsp
points and the resultant value of %rsp
will be the value we need
for argv
(remember, argv
is an array of pointers and to pass that as an
argument in C we pass the location of that array). In assembler this is
pop %rdi
mov %rsp, %rsi
Our stack started off being 16-byte aligned but is now 8-byte aligned due to the
pop
instruction. This means we need to re-align the stack on a 16-byte
boundary because that's what is expected upon entry to main
. This can be
achieved in a few ways, but we'll be explicit about what we're doing by AND'ing
%rsp
with a bit mask that will preserve all but the last nibble (-16
in
two's complement), guaranteeing 16-byte alignment. Once done, we're ready to
call main
.
pop %rdi
mov %rsp, %rsi
and $-16, %rsp
call main
exit syscall number
Our last item is to figure out what the syscall number for exit is. 64-bit
syscalls on Linux are defined in the header /usr/include/asm/unistd_64.h
,
which has the following line
#define __NR_exit 60
Now we save off the return value from main and call the exit
syscall like so
mov %rax, %rdi
movq $60, %rax
syscall
I was given a protip to specify the size when mov
ing immediate values rather
than relying on the default size. This is why movq
is used on the immediate
value of 60
.
_start.S
will look something like this
.globl _start # Declare global _start symbol
_start:
pop %rdi # Place argc as first arg to main
mov %rsp, %rsi # Place location of argv[] as second arg to main
and $-16, %rsp # Realign stack on a 16-byte boundary
call main # Call main function
mov %rax, %rdi # Copy return value from main as argument to exit(2)
movq $60, %rax # Set rax to syscall number for exit
syscall # Execute the syscall specified in rax
Assembling assembly
We now have an assembly file that we need to assemble. Let's try assembling it by itself to see what happens
$ gcc -o _start _start.S
/tmp/ccZH6Hhm.o: In function `_start':
(.text+0x0): multiple definition of `_start'
/usr/lib/gcc/x86_64-redhat-linux/4.8.2/../../../../lib64/crt1.o:(.text+0x0): first defined here
/usr/lib/gcc/x86_64-redhat-linux/4.8.2/../../../../lib64/crt1.o: In function `_start':
(.text+0x20): undefined reference to `main'
/tmp/ccZH6Hhm.o: In function `_start':
(.text+0x1): undefined reference to `main'
collect2: error: ld returned 1 exit status
Here we compiled with glibc (because we left out -nostdlib) and we can see that
_start is in fact defined by the normal standard library because gcc
tells us
that the _start symbol was first defined in the crt1.0 file. Hopefully things
are starting to make sense as to what glibc is providing and why we get errors
when we don't link against it. You can also see there is an undefined reference
to main which should be expected since we call the main function but we haven't
provided gcc
with the code for it yet.
Now, let's compile without glibc and see what output we get
$ gcc -nostdlib -ffreestanding -o _start _start.S
/tmp/ccDUeM2Z.o: In function `_start':
(.text+0x1): undefined reference to `main'
collect2: error: ld returned 1 exit status
Awesome, we don't get anymore errors about multiple definitions of start and we
still get the expected error about main. However, we really want an object file
so that we can then link it to the code where main is defined. You can instruct
gcc
to stop before the linking phase which will give you an object file.
$ gcc -nostdlib -ffreestanding -c -o _start.o _start.S
Now we don't get any errors since gcc
didn't try to resolve any symbols.
We'll do the same thing with our main_test.c file to get an object file with main defined inside of it
$ gcc -nostdlib -ffreestanding -c -o main_test.o main_test.c
Linking objects together
So we're now at the point where we have one file defining the _start symbol and referencing the main symbol, and one file which defines the main symbol. We need to link them together so that the unresolved symbols will be fixed up so that we can execute our code with no seg faults.
First, let's verify what we have. If we dump the symbol table for _start.o we should see the _start symbol and an undefined main symbol
$ readelf -s _start.o
Symbol table '.symtab' contains 6 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 SECTION LOCAL DEFAULT 1
2: 0000000000000000 0 SECTION LOCAL DEFAULT 3
3: 0000000000000000 0 SECTION LOCAL DEFAULT 4
4: 0000000000000000 0 NOTYPE GLOBAL DEFAULT 1 _start
5: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND main
The "Ndx" column indicates whether the symbol is undefined or not, and as we expected, the main symbol is undefined. Now, let's look at main_test.o
$ readelf -s main_test.o
Symbol table '.symtab' contains 9 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS main_test.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 2
4: 0000000000000000 0 SECTION LOCAL DEFAULT 3
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 0 SECTION LOCAL DEFAULT 6
7: 0000000000000000 0 SECTION LOCAL DEFAULT 4
8: 0000000000000000 23 FUNC GLOBAL DEFAULT 1 main
Likewise we now have a defined main symbol. Now, let's link them together to get a working binary!
We can pass object files to gcc
and it knows not to preprocess, compile, or
assemble them. We still need to specify not to use glibc because gcc
will
automatically link against it otherwise, and in our case we want to link only
main_test.o and _start.o:
$ gcc -nostdlib -ffreestanding -o test_welibc _start.o main_test.o
And now let's look at the final symbol table to be sure everything is defined
$ readelf -s test_welibc
Symbol table '.symtab' contains 13 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000400120 0 SECTION LOCAL DEFAULT 1
2: 0000000000400144 0 SECTION LOCAL DEFAULT 2
3: 0000000000400168 0 SECTION LOCAL DEFAULT 3
4: 0000000000400180 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 0 FILE LOCAL DEFAULT ABS main_test.c
7: 0000000000000000 0 FILE LOCAL DEFAULT ABS
8: 000000000040015c 0 NOTYPE GLOBAL DEFAULT 2 _start
9: 0000000000601000 0 NOTYPE GLOBAL DEFAULT 4 __bss_start
10: 0000000000400144 23 FUNC GLOBAL DEFAULT 2 main
11: 0000000000601000 0 NOTYPE GLOBAL DEFAULT 4 _edata
12: 0000000000601000 0 NOTYPE GLOBAL DEFAULT 4 _end
Removing startup files and standard libraries
We can add two more compiler flags to get rid of a few more system defaults.
First, we'll direct gcc
not to use any of the standard system startup files
with -nostartfiles
. Second, we don't want to use any of the default system
libraries since we are writing our own standard C library; this is done with the
-nodefaultlibs
flag. From the man pages description of the -nostdlib
flag
we're already using, it seems to be a combination of -nostartfiles
and
-nodefaultlibs
, but we'll include them just to be sure.
Our final compilation looks like
$ gcc -nostdlib -ffreestanding -nostartfiles -nodefaultlibs -o test_welibc _start.o main_test.o
Running glibc free
So, we haven't gotten any errors during preprocessing, compiling, assembling, or linking. It's time to test our binary that doesn't use glibc to see if it runs properly
$ ./test_welibc
$ echo $?
42
Sweet! Now we can continue on with the development of the rest of the library since we can successfully run the most basic code when linking against it.
comments powered by Disqus