aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Boot-explained.txt24
-rw-r--r--Makefile-explained.txt18
2 files changed, 24 insertions, 18 deletions
diff --git a/Boot-explained.txt b/Boot-explained.txt
index 1132f16..403ba15 100644
--- a/Boot-explained.txt
+++ b/Boot-explained.txt
@@ -1,21 +1,27 @@
-When RaspberryPi boots, it searches the first partition on SD card (which should be formatted FAT) for it's firmware and configuration files, loads them and executes them. The firmware then searches for the kernel image file. The name of the looked for file can be kernel.img, kernel7.img, kernel8.img (for 64-bit mode) or something else, depending on configuration and firmware used (rpi-open-firmware looks for zImage). The image is then copied to some address (which should be 0x8000 for 32-bit kernel, but is 0x2000000 in rpi-open-firmware and 0x10000 in qemu (version 2.9.1)) and jumped to on all cores. 3 arguments are passed to the kernel: first (passed in r0) is 0; second (passed in r1) is machine type; third (passed in r2) is the address of FDT or ATAGS structure describing the system or 0.
-Pis, that support aarch64, can also boot directly into 64-bit mode, in which case the image gets loaded at 0x80000. We're not using 64-bit mode in this project.
-Qemu can be used to emulate RaspberryPi, in which case kernel image and memory size are provided to the emulator on the command line. Qemu can also load kernel in the form of an elf file, in which case it's load address is determined based on information in the elf.
+When RaspberryPi boots, it searches the first partition on SD card (which should be formatted FAT) for its firmware and configuration files, loads them and executes them. The firmware then searches for the kernel image file. The name of the looked for file can be kernel.img, kernel7.img, kernel8.img (for 64-bit mode) or something else, depending on configuration and firmware used (rpi-open-firmware looks for zImage). The image is then copied to some address and jumped to on all cores. Address should be 0x8000 for 32-bit kernel, but in reality is 0x2000000 in rpi-open-firmware and 0x10000 in qemu (version 2.9.1). 3 arguments are passed to the kernel: first (passed in r0) is 0; second (passed in r1) is machine type; third (passed in r2) is the address of FDT or ATAGS structure describing the system or 0 as default.
+PIs that support aarch64 can also boot directly into 64-bit mode. Then, the image gets loaded at 0x80000. We're not using 64-bit mode in this project.
+Qemu can be used to emulate RaspberryPi, in which case kernel image and memory size are provided to the emulator on the command line. Qemu can also load kernel in the form of an elf file, in which case its load address is determined based on information in the elf.
-Our kernel has been executed on qemu emulating RaspberryPi 2 as well as on real RaspberryPi 3 running rpi-open firmware (although not every functionality works everywhere). To quicken running new images of the kernel on the board, a simple bootloader has been written by us, which can be run from the SD card instead of the actual kernel. It reads the kernel image from uart, and executes it. The bootloader can also be used within qemu (although there are problems with passing keyboard input to the kernel once it's running).
+Our kernel has been executed on qemu emulating RaspberryPi 2 as well as on real RaspberryPi 3 running rpi-open firmware (although not every functionality works everywhere). To quicken running new images of the kernel on the board, a simple bootloader has been written by us, which can be run from the SD card instead of the actual kernel. It reads the kernel image from uart, and executes it. The bootloader can also be used within qemu, but there are several problems with passing keyboard input to the kernel once it's running.
Both bootloader and kernel are split into 2 stages.
-In case of the loader it is due to the fact, that the the actual kernel read by it from UART is supposed to be written at 0x8000. If the loader also ran from 0x8000 or a cloase address, it could possibly overwrite it's own code while writing kernel to memory. To avoid this, the first stage of the loader first copies it's second stage embedded in it to address 0x4000. Then it jumps to that second stage, which reads kernel image from uart, writes it at 0x8000 and jumps to it. Arguments (r0, r1, r2) are preserved and passed to the kernel. Second stage of the bootloader is intended to be kept small enough to fit between 0x4000 and 0x8000. Atags structure, if present, is guaranteed to end below 0x4000, so it should not get overwritten by loader's stage2.
-The loader protocol is simple: first, size of the kernel is sent through UART (4 bytes, little endian). Then, the actual kernel image. Our program pipe_image is used to prepend kernel image with it's size.
-In case of kernel, it is desired to have image run from 0x0, because that's where the interrupt vector table is under default settings. This is also achieved by splitting into 2 stages. Stage 1 is loaded at some higher address. It has second stage image embedded in it. It copies it to 0x0 and jumps to it. What gets more complicated, than in the loader, is the handling of ATAGS structure. Before copying stage 2 to 0x0, stage 1 first checks if atags is present and if so, it is copied to some location high enough, that it won't be overwritten by stage 2 image. Whenever the memory layout is modified, it should be checked, if there is a danger of ATAGS being overwritten by some kernel operations before it is used. In current setup, new location chosen for ATAGS is always below the memory later used as the stack and it might overlap memory later used for translation table, which is not a problem, since kernel only uses ATAGS before filling that table.
+In case of the loader it is due to the fact, that the the actual kernel read by it from UART is supposed to be written at 0x8000. If the loader also ran from 0x8000 or a close address, it could possibly overwrite it's own code while writing kernel to memory. To avoid this, the first stage of the loader first copies its second stage embedded in it to address 0x4000. Then, it jumps to that second stage, which reads kernel image from uart, writes it at 0x8000 and jumps to it. Arguments (r0, r1, r2) are preserved and passed to the kernel. Second stage of the bootloader is intended to be kept small enough to fit between 0x4000 and 0x8000. Atags structure, if present, is guaranteed to end below 0x4000, so it should not get overwritten by loader's stage2.
+
+The loader protocol is simple: first, size of the kernel is sent through UART (4 bytes, little endian). Then, the actual kernel image. Our program pipe_image is used to prepend kernel image with its size.
+
+In case of kernel, it is desired to have image run from 0x0, because that's where the interrupt vector table is under default settings. This is also achieved by splitting it into 2 stages.
+
+Stage 1 is loaded at some higher address. It has second stage image embedded in it. It copies it to 0x0 and jumps to it. What gets more complicated compared to loader, is the handling of ATAGS structure. Before copying stage 2 to 0x0, stage 1 first checks if atags is present and if so, it is copied to some location high enough, that it won't be overwritten by stage 2 image. Whenever the memory layout is modified, it should be checked, if there is a danger of ATAGS being overwritten by some kernel operations before it is used. In current setup, new location chosen for ATAGS is always below the memory later used as the stack and it might overlap memory later used for translation table, which is not a problem, since kernel only uses ATAGS before filling that table.
+
When stage 1 of the kernel jumps to second stage, it passes modified arguments: first argument (r0) remains 0 if ATAGS was found and is set to 3 to indicate, that ATAGS was not found. Second argument (r2) remains unchanged. Third argument (r2) is the current address of ATAGS (or remains unchanged if no ATAGS was found).
If support for FDT is added in the future, it must also be done carefully, so that FDT doesn't get overwritten.
-At the start of the stage 2 of the kernel, there is the interrupt vector table. It's first entry is the reset vector, which is not normally unused. In our case, when stage 1 jumps to 0x0, to first instruction of stage 2, it jumps to that vector, which then calls the setup routine.
+
+At the start of the stage 2 of the kernel, there is the interrupt vector table. It's first entry is the reset vector, which is not normally unused. In our case, when stage 1 jumps to 0x0, first instruction of stage 2, it jumps to that vector, which then calls the setup routine.
In both loader and the kernel, at the beginning of stage1 it is ensured, that only one ARM core is executing.
-It's worth noting, that in first stages the loop that copies the embedded second stage is intentionally situated after the blob in the image. This way, this loop will not overwrite itself with the data it is copying, since the stage 2 is always copied to some lower address (to 0x0 in case of kernel and to 0x4000 in case of loader - we assume stage 1 won't be loaded below 0x4000).
+It's worth noting, that in first stages the loop that copies the embedded second stage is intentionally situated after the blob in the image. This way, this loop will not overwrite itself with the data it is copying, since the stage 2 is always copied to some lower address. It copies to 0x0 in case of kernel and to 0x4000 in case of loader - we assume stage 1 won't be loaded below 0x4000.
Qemu, stock RaspberryPi firmware and rpi-open-firmware all load image at different addresses. Although stock firmware is not used in this project, our loader loads kernel at 0x8000, where the stock firmware would. Because of that, it is desired, that image is able to run, regardless of where it was loaded at. This was realized by writing first stages of loader and kernel in careful, position-independent assembly. The starting address in corresponding linker scripts is irrelevant. The stage 2 blobs are embedded using .incbin assembly directive. Second stages are written normally in C and compiled as position-dependent for their respective addresses.
diff --git a/Makefile-explained.txt b/Makefile-explained.txt
index 0008095..0a624d0 100644
--- a/Makefile-explained.txt
+++ b/Makefile-explained.txt
@@ -1,7 +1,7 @@
To maintain order, all files created with the use of make, that is binaries, object files, natively executed helper programs, etc. get placed in build/.
-Our project contains 2 Makefiles: one in it's root directory and one in build/. The reason is that it is possible to use Makefile to simply, elegantly and efficiently produce files in the same directory where it is, but to produce files in directory other than Makefile's own, it requires this directory to be specified in many rules across the Makefile and in general it complicates things. Also, a problem arises when trying to link objects not from within the current directory. If an object is referenced by name in linker script (which is a frequent practice in our scripts) and is passed to gcc with a path, then it'd need to also appear with that path in the linker script.
-Because of that a Makefile in build/ is present, that produces files into it's own directory and the Makefile in project's root is used as a proxy to that first one - it calls make recursively in build/ with the same target it was called with.
+Our project contains 2 Makefiles: one in it's root directory and one in build/. The reason is that it is easier to use Makefile to simply, elegantly and efficiently produce files in the same directory where it is. To produce files in directory other than Makefile's own, it requires this directory to be specified in many rules across the Makefile and in general it complicates things. Also, a problem arises when trying to link objects not from within the current directory. If an object is referenced by name in linker script (which is a frequent practice in our scripts) and is passed to gcc with a path, then it'd need to also appear with that path in the linker script.
+Because of that a Makefile in build/ is present, that produces files into it's own directory and the Makefile in project's root is used as a proxy to that first one - it calls make recursively in build/ with the same target it was called with. These changes makes it easier to read.
From now on only Makefile in build/ will be discussed.
@@ -10,9 +10,9 @@ In the Makefile, variables with the names of certain tools and their command lin
All variables discussed below are defined using := assignment, which causes them to only be evaluated once instead of on every reference to them.
Objects that should be linked together to create each of the .elf files are listed in their respective variables. I.e. objects to be used for creating kernel_stage2.elf are all listed in KERNEL_STAGE2_OBJECTS. When adding a new source file to the kernel, it is enough to add it's respective .o file to that list to make it compile and link properly. No other Makefile modifications are needed.
-In a simillar fashion, RAMFS_FILES variable specifies files, that should be put in the ramfs image, that will be embedded in the kernel. Adding another file only requires listing it there. However, if the file is to be found somewhere else that build/, it might be useful to use the vpath directive to tell make where to look for it.
+In a similar fashion, RAMFS_FILES variable specifies files, that should be put in the ramfs image, that will be embedded in the kernel. Adding another file only requires listing it there. However, if the file is to be found somewhere else that build/, it might be useful to use the vpath directive to tell make where to look for it.
-Variables dirs and dirs_colon are defined to store list of all directories within src/, separated with spaces and colons, respectively. dirs_colons are used for vpath directive. dirs are used in ARM_FLAGS to pass all the directories as include search paths to gcc. empty and space are helper variables - defining dirs_colon could be achieved without them (but it's clearer this way).
+Variables dirs and dirs_colon are defined to store list of all directories within src/, separated with spaces and colons, respectively. dirs_colons are used for vpath directive. 'dirs' variable is used in ARM_FLAGS to pass all the directories as include search paths to gcc. empty and space are helper variables - defining dirs_colon could be achieved without them (but it's clearer this way).
The vpath directive tells make to look for assembler sources, C sources and linker scripts in all direct and indirect subdirectories of src/ (including itself). All other files shall be found/created in build/.
@@ -22,24 +22,24 @@ The generic rule for compiling C sources uses cross-compiler or native compiler
The generic rules for making a stripped binary image out of elf file, for assembling an assembly file, for making an arbitrary file a linkable object and for linking objects are ARM-only.
-In C world it is possible to embed a file in an executable by using objcopy to create an object file from it and then linking that object file into the executable. In this project, at the current time, this is used only for embedding ramfs in the kernel (incbin is used for embedding kernel and loader second stages in their first stages), but a generic rule for making a binary image into object file is present in case it is needed somewhere else again.
+In C world it is possible to embed a file in an executable by using objcopy to create an object file from it and then linking that object file into the executable. In this project, at the current time, this is used only for embedding ramfs in the kernel (incbin is used for embedding kernel and loader second stages in their first stages). Generic rule for making a binary image into object file is present, in case it is needed somewhere else again.
To link elf files, the generic rule is combined with a rule that specifies the elf's objects. Objects are listed in variables whenever more than one of them is needed.
-At this point in the Makefile, the dependence of objects created from assebmly on files referenced in the assembly source via incbin is marked.
+At this point in the Makefile, the dependence of objects created from assembly on files referenced in the assembly source via incbin is marked.
Simple ram filesystem is created from files it should contain with the use of our own simple tool - makefs.
-Another 2 rules specifie how native programs (for the machine we're working on) are to be linked.
+Another 2 rules specify how native programs (for the machine we're working on) are to be linked.
Rule qemu-elf runs the kernel in qemu emulating RaspberryPi 2 with 256MiB of memory by passing the elf file of the kernel to the emulator.
Rule qemu-bin does the same, but passes the binary image of the kernel to qemu.
-Rule qemu-loader does the same, but first passes the binary image of the bootloader to qemu and the actual kernel is piped to qemu's standard input, received by bootloader as uart data and run. This method currently makes it impossible to pass any keyboard input tu the kernel once it's running
+Rule qemu-loader does the same, but first passes the binary image of the bootloader to qemu and the actual kernel is piped to qemu's standard input, received by bootloader as uart data and run. This method currently makes it impossible to pass any keyboard input to kernel once it's running.
Rule run-on-rpi pipes the kernel through uart, assuming it is available under /dev/ttyUSB0, and then opens a screen session on that interface. This allows for executing the kernel on the Pi connected through UART, provided that our bootloader is running on the board.
Rule clean removes all the files generated in build/.
-Ruled that don't generate files are marked as PHONY.
+Rules that don't generate files are marked as PHONY.