Chapter 1: Toolchain - COMPLETE
This commit is contained in:
parent
bbd88e8567
commit
0df19ec2b7
415
doc/1_Toolchain.md
Normal file
415
doc/1_Toolchain.md
Normal file
@ -0,0 +1,415 @@
|
||||
# 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.
|
@ -3,4 +3,5 @@
|
||||
**Table of Contents**
|
||||
|
||||
[Chapter 0: Introduction](0_Introduction.md)<br>
|
||||
[Chapter 1: Toolchain](1_Toolchain.md)<br>
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user