Don't worry, typos can always happen!
Unfortunately, I didn't have enough time to complete my follow-up post until now, and it got much longer than I anticipated. So here we go...
First, I noticed that your source tree structure looks to me a bit suboptimal. Maybe you did some things this way by design, I don't know, but I'd recommend adopting a more maintainable structure anyway. Specifically, you'd have at the top-level one directory per "subproject", i.e. your kernel, your drivers (if and only if you are writing a microkernel), your standard library (I guess this is your "userspace" directory), your base utilities (in the future), your base services (again in the future), etc. More complex utilities and services can be granted a separate top-level directory each. The "static" misc files, which don't need any processing (building) and are just used "as is" in the sysroot, should be put in a separate top-level directory (e.g. "static" or "misc" or just the top-level directory itself), and then copied to the sysroot when building. Inside each subproject, you should have "include" for headers, and the source should be put inside either "src" or the subproject directory itself. I don't think it's a good idea to put "include" inside "src", as it's a bit unconventional. Each subproject should also have its own Makefile. The architecture-specific files (all assembly files, your linker script, and possibly some C files) should be put inside a subdirectory of "src", with one such directory per supported architecture and possibly, later in the future, additional directories for code that is shared between two architectures, but not the rest (for example i386 and x86_64, but not ARM, ARM64, RV32, RV64, MIPS or others). For example, you would have something like this (names enclosed in <> are placeholder names):
Code: Select all
LICENSE
Makefile
README
TODO
bochsrc
docs
|
+- ...
kernel
|
+- Makefile
|
+- include
| |
| +- ...
|
+- src
|
+- arch
| |
| +- i386
| | |
| | +- linker.ld
| | |
| | +- ...
| |
| +- ...
|
+- ...
<libsys> (or whatever you name your standard library, e.g. for *nix it's usually "libc")
|
+- Makefile
|
+- include
| |
| +- ...
|
+- src
|
+- arch
| |
| +- i386
| | |
| | +- ...
| |
| +- ...
|
+- ...
static
|
+- boot
|
+- grub
|
+- grub.cfg
<utilbase> (or whatever you name your base utilities, e.g. for *nix it's usually "*utils")
Some other notes concerning filenames: Makefile filenames (no pun intented) should preferably start with a capital 'M'. A README filename should preferably be all capitals.
Another issue I found is your build system. You provide no way to clean the source tree. I have to note that I accidentally deleted "/boot/grub/grub.cfg" the first time I manually did the cleaning. I'd recommend the top-level Makefile to contain the targets "all" (which builds the sysroot using "make all" and "make install" of the Makefiles of each subproject), "clean" (which cleans the build files using "make clean" of the Makefiles of each subproject, and also cleans the sysroot), "iso" (which builds the .iso file from the sysroot), and maybe "bximage" (if you fix the command). In each subproject, the Makefile should have the "all" target (which builds inside that directory), the "clean" target (which cleans the build files but not the installed files) and the "install" target (which installs to $DESTDIR). The top-level Makefile should set $DESTDIR to tell the lower-level Makefiles to install to the sysroot.
One more tip: For building multiple C and/or assembly files in a subproject, define an $OBJECTS variable to contain the object file filenames and use a wildcard rule. Like this:
Code: Select all
DESTDIR ?=
OBJECTS := \
<object 1> \
<object 2> \
... \
<object n> \
.PHONY: all clean install
.SUFFIXES: .asm .c .o
all: octos.bin
clean:
# rm files generated during building
install:
# cp kernel to $(DESTDIR)/boot
octos.bin: $(OBJECTS)
$(CC) -T linker.ld -o octos.bin -ffreestanding -nostdlib $(OBJECTS) -lgcc
.asm.o:
nasm -f elf32 -o $@ $<
.c.o:
$(CC) $(CFLAGS) -c -o $@ $<
Edit: For architecture-specific files, you should define more such variables, e.g. $OBJECTS_I386 or $OBJECTS_X86_64. Depending on the target platform specified, you should choose one of these variables at build time (it could be some Makefile conditionals, or an external script, or something else; I don't know of a standard way for this). Then, that variable should be appended to the prerequisites and to the inputs of the command of the target of your final output, in this case "octos.bin". Like this:
Code: Select all
octos.bin: $(OBJECTS) $(OBJECTS_ARCH_SELECTED)
$(CC) -T linker.ld -o octos.bin -ffreestanding -nostdlib $(OBJECTS) $(OBJECTS_ARCH_SELECTED) -lgcc
One more thing, you can also pass -MD to $(CC), so it generates Makefile rules based on the include directives in each individual file. If the compiled file is named "main.c", the generated file will be named "main.d". As for the assembly files, I haven't looked into how nasm does this, but you don't really need to use includes in assembly. Just assemble them as separate units (with the help of the wildcard rules). Then at the bottom of your Makefile include those generated files like this:
Don't forget to remove them during cleaning!
One third issue I found is that you don't name/place your files and functions based on what they do. For example, "src/kernel/kio.c" is effectively a VGA text mode driver (which has no input and is only one kind of possible output). Many of the files in your "src/drivers" directory aren't really drivers. For example, it's not called a driver the thing that setups the GDT or the TSS; this is an integral part of the core kernel. Your "multiboot.asm" is also part of the kernel, and usually you'd put this inside "boot.asm", along with the other pre-main() code, though it's absolutely fine to do it as a separate file assembled separately. You put your pre-main() code in "main.asm" and not "boot.asm", which is a misnomer since the main() function is defined elsewhere. You will probably for now remember these things (and probably others I didn't point out), but you might not remember them if you take a break of 3 months or work on other stuff for a similar period, and then return to it. I'd recommend you take code maintainability more seriously from the start to avoid headaches later.
As for your VGA text mode driver, since I provided it as an example, I'd recommend for now that you'd use said code as a backend to a more generic kernel logging mechanism (possibly with other backends in place, e.g. serial, possibly in the future a log file, possibly in the long future a network resource, etc). Later, you could use it as a shell output, or you could stop supporting text mode at all and move to graphical modes instead.
I imagine you are overwhelmed by this post, as it's indeed a lot of information. I haven't looked at the implementations of each feature separately (e.g. GDT, IDT, TSS, ATA, PMM, etc), but I think that code maintainability matters more in the long run than bugs in feature implementations (though they do matter too). I'll leave the bugs to you.