mittos64/doc/1_Toolchain.md

416 lines
12 KiB
Markdown

# Chapter 1 - Setting up a toolchain
In this chapter we'll build a docker image which contains all the tools
we need to build and emulate our OS.
We'll also make some helper scripts to run commands in the Docker
container and for running the emulator and debugger.
## Docker image
### Why docker?
I've heard the name Docker thrown around a lot the last year or two, but
only just recently started to look into it. The idea of using it for
compiling an operating system came to me almost immediately.
Docker lets you run processes inside a well defined, isolated and
portable, linux-based environment. What's there not to like?
So, let's build a Docker image for osdeving.
### What we want
For now, I want the following in the image:
- binutils
- gcc
- make
- grub
- xorriso
- qemu
- gdb
In order to get a known compiler configuration, we will be building
`binutils` and `gcc` from source. At this point, we'll only use a base
configuration, and could therefore probably use the versions that come
with the docker base linux image. Later, however, we'll patch them to
add new targets for compiling native usermode code for our OS, so we
might as well get the practice of compiling.
`Make` is just for simplifying the build process. An indispensable tool,
really, but more on that later.
`Grub` and `xorriso` is used to generate a bootable cdrom ISO with our
kernel. We'll need to make sure we get `grub` with BIOS support, though,
because that's what `qemu` expects.
`Qemu` for emulating. We won't need all of `qemu`, but most package
managers will let you install just one or a few system emulators. In our
case, we want `qemu-system-x86_64` specifically.
Finally `gdb` can attach to qemu and be used to inspect and change
memory, variables, code, registers. It has saved me inumerable times
already.
### Dockerfile
I chose to build my image on top of alpine linux, because that seems to
be generally accepted as best practice.
Alpine also happens to be built on musl c library, which is what I plan
to port to this OS. Not that it matters...
So...
`toolchain/Dockerfile`
```dockerfile
FROM alpine:3.6
ADD build-toolchain.sh /opt/build-toolchain.sh
RUN /opt/build-toolchain.sh
ENV PATH "/opt/toolchain:$PATH"
ENV MITTOS64 "true"
ENV BUILDROOT "/opt/"
WORKDIR /opt
```
This simply copies over the installation script, runs it and then sets a
few environment variables.
The installation script looks like this:
`toolchain/build-toolchain.sh`
```bash
#!/bin/sh -e
apk --update add build-base
apk add gmp-dev mpfr-dev mpc1-dev
apk add make
apk add grub-bios xorriso
apk add gdb
apk --update add qemu-system-x86_64 --repository http://dl-cdn.alpinelinux.org/alpine/v3.7/main
rm -rf /var/cache/apk/*
target=x86_64-elf
binutils=binutils-2.29
gcc=gcc-7.2.0
cd /opt
wget http://ftp.gnu.org/gnu/binutils/${binutils}.tar.gz
tar -xf ${binutils}.tar.gz
mkdir binutils-build && cd binutils-build
../${binutils}/configure \
--target=${target} \
--disable-nls \
--disable-werror \
--with-sysroot \
make -j 4
make install
cd /opt
wget http://ftp.gnu.org/gnu/gcc/${gcc}/${gcc}.tar.gz
tar -xf ${gcc}.tar.gz
mkdir gcc-build && cd gcc-build
../${gcc}/configure \
--target=${target} \
--disable-nls \
--enable-languages=c \
--without-headers \
make all-gcc all-target-libgcc -j 4
make install-gcc install-target-libgcc
apk del build-base
cd /
rm -rf /opt
```
First we use the alpine package manager `apk` to install the things we need for
compiling, `build-base` - which is compilers and stuff, and some libraries
needed to compile `gcc`. Then we install the packages discussed above and
finally download, configure, make and install binutils and gcc. Note that make
is installed specifically, even though it's included in build-base. This is so
that we can uninstall build-base to save room, and still have make available.
> #### A note about qemu versions
> You'll note that qemu is installed from a different repository than the rest
> of the packages. This is because of a problem with the gdb server in some
> qemu versions. For some versions, gdb can't follow when qemu switches
> processor mode, e.g. from 32 to 64 bit execution. You will then get some
> error message about "g packet size" and some numbers. The way to solve this
> is to disconnect from the remote debugging, change architecture manually, and
> then reconnect:
> ```
> (gdb) disconnect
> (gdb) set architecture i386:x86_64:intel
> (gdb) target remote :1234
> ```
> Or you could make sure you're running qemu version 2.10 or later, which seems
> to have fixed this problem.
> At the time of writing, the lates alpine docker image is version 3.6, which
> installs qemu version 2.8 by default. Therefore, we manually choose the
> repository for alpine 3.7 instead.
The configuration flags are well described in the [GCC
Cross-Compiler](http://wiki.osdev.org/GCC_Cross-Compiler) article over at
[osdev.org](http://osdev.org), so I recommend you read that if you didn't
already. This also gives us a good base for later adding a custom build target.
The image can be built with `docker build -t mittos64 toolchain/.` and
when done, you can run a command inside it to test that it works:
```
$ docker run --rm mittos64 x86_64-elf-gcc --version
x86_64-elf-gcc (GCC) 7.2.0
Copyright (C) 2017 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
```
## Docker helper script
In order to compile our code inside Docker, we need to mount our source
directory to the container. This can be done with the -v flag, but at
this point things are starting to look messy, so let's write a script
for it.
I simply named it `d` and put it in the project root directory.
`d`
```bash
#!/usr/bin/env bash
imagename=mittos64
buildroot="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)"
if [[ $(docker ps -q -f name=${imagename}-run) ]]; then
docker exec -it -u $(id -u):$(id -g) ${imagename}-run "$@"
else
docker run -it --rm -v ${buildroot}:/opt --name ${imagename}-run -u $(id -u):$(id -g) ${imagename} "$@"
fi
```
This will run any command inside the docker container as the calling user:
$ ./d qemu-system-x86_64 --version
QEMU emulator version 2.8.1
Copyright (c) 2003-2016 Fabrice Bellard and the QEMU Project developers
Furthermore. If a command is already running in the container, the next
invocation of `d` will not launch a new container, but instead connect
to the currently running one. This means you can e.g. run `qemu` and
`gdb` inside the same container, so that they may talk to each other.
The command will mount the directory the `d` script resides in to `/opt`
in the container, which also is the default working directory, so we'll
have direct access to all our source.
## Making a bootable ISO file
When the kernel is compiled, we need to get it to a computer or emulator
somehow. `Qemu` can actually load a MultiBoot 1 compatible kernel
directly, but I believe making a bootable ISO is a more robust way to
go. This is obviously something we will need to do a lot of times, so
it's scripting time.
`toolchain/mkiso`
```bash
#!/bin/sh -e
if [ -z ${MITTOS64+x} ]; then >&2 echo "Unsupported environment! See README"; exit 1; fi
sysroot=${BUILDROOT}sysroot
iso=${BUILDROOT}mittos64.iso
mkdir -p ${sysroot}
grub-mkrescue -o ${iso} ${sysroot}
```
The first two lines need some explanation. First of all, I normally
write all my scripts for `bash` Alpine linux, however, does not include
`ash`by default. So instead we are going for `sh`.
The second line checks if the $MITTOS64 environment variable is set. If
it's not, execution stops immediately. This will be a feature of most
scripts within the project. This is just to avoid messing anything up
on your own computer. Remember that this variable was defined by the
Dockerfile.
`BUILDROOT` was also defined in the Dockerfile.
The rest of the script builds a `sysroot` directory (that is where our
boot filesystem will live, for now it's empty...) and then turns it all
into an ISO with grub installed.
## Running the emulator
We'll test the kernel using `qemu`.
`Qemu` has a lot of command line flags.
We don't want to type those in all the time.
Script time:
`toolchain/emul`
```bash
#!/bin/sh -e
if [ -z ${MITTOS64+x} ]; then >&2 echo "Unsupported environment! See README"; exit 1; fi
iso=${BUILDROOT}mittos64.iso
${BUILDROOT}toolchain/mkiso
qemu-system-x86_64 -s -S -cdrom ${iso} -curses
```
This should be simple enough. After the environment check, it runs the
`mkiso` script and then starts `qemu` with the options:
- -s to start a gdb server at telnet port 1234
- -S to freeze the cpu at startup and wait for a command to continue
- -cdrom ${iso} to mount our ISO as a cd
- -curses to output the screen (in VGA text mode) to the terminal
Later we'll add stuff like multiple cpus and VNC output to be able to
use graphics modes, but this is good enough for now.
## Debugger
Finally, we add a script to start `gdb` with some initial settings
`toolchain/gdb`
```bash
#!/bin/sh -e
if [ -z ${MITTOS64+x} ]; then >&2 echo "Unsupported environment! See README"; exit 1; fi
/usr/bin/gdb -q -x ${BUILDROOT}toolchain/gdbinit
```
This just runs `gdb` and tells it to read and execute
`toolchain/gdbinit`. Normally, you'd probably use a `.gdbinit` either
in your home directory, or in the project root, but I wanted it to be
visible (filenames starting with `.` are hidden in UNIX-like systems)
and together with the rest of the toolchain stuff. Hence the script -
which overloads `gdb` since it will come earlier in $PATH.
So, the important stuff in this section is actually the `gdbinit` file.
`toolchain/gdbinit`
```gdb
set prompt \033[31m(gdb) \033[0m
set disassembly-flavor intel
target remote :1234
define q
monitor quit
end
define reg
monitor info registers
end
define reset
monitor system_reset
end
```
This script does the following:
- Colors the gdb prompt read for improved visibility
- Makes gdb use intel assembly syntax, rather that AT&T. Personal preference.
- Connects to the `qemu` gdb server at port 1234
- redefines the `q` command to stop the emulator (this will kill `gdb`
as well). If you want to, you can still use `(gdb) quit` to quit just
the debugger.
- Defines a `reg` command which pulls in the register information from
`qemu`. This is more detailed than the `gdb` register output.
- Defines a `reset` command to reboot the emulator.
## Trying it all out
### Emulator
To make sure everything works, open up a terminal window and run
$ d emul
After a second or two, you should get a blank screen with the text `VGA Blank mode`.
That means qemu is paused and waiting for a command to start.
You could start it by switching to the qemu monitor with `META+2` (if
you don't have a meta key, press and release `ESC` immediately followed
by `2`) and running
(qemu) c
Then you can switch back to the VGA output with `META+1`.
Once the emulator is running, you should soon see the GRUB start screen:
`GNU GRUB version 2.02` followed by some text about tab completion and
then a grub prompt
grub>
You can exit the emulator by going to the monitor and issuing
(qemu) q
### Debugger
To check that the debugger works, start the emulator again with
$ d emul
Then open another terminal window and run
$ d gdb
You should se some text and then a `(gdb)` prompt. The emulator window
should still show `VGA Blank mode`.
Now run
(gdb) c
and the emulator should start running and bring you to the `grub` prompt.
When you're there, pause executing with `CTRL+c` (in gdb), which brings
back the prompt.
You can now inspect the processor registers with
(gdb) reg
EAX=00000000 EBX=00000000 ECX=07fa0880 EDX=00000031
ESI=00000000 edI=07fa0880 EBX)00001ff0 ESP=00001ff4
[...]
XMM06=00000000000000000000000000000000 XMM07=00000000000000000000000000000000
Finally, run
(gdb) q
and notice that both the emulator and gdb stops.