C++ kernel design questions
- Steve the Pirate
- Member
- Posts: 152
- Joined: Fri Dec 15, 2006 7:01 am
- Location: Brisbane, Australia
- Contact:
C++ kernel design questions
Hi,
I have been writing a kernel in C++ for a while now (I started months ago, but have only been working on it in very short bursts)... But I'm not sure if I'm structuring the kernel the best way possible, so I'd like to ask what is the best way to do it.
Basically, the way I'm doing it now is that I have a kernel class, that has instances of the GDT, IDT etc. class etc. as members. The main function makes an instance of the kernel object, which makes the other instances in its constructor. But the problem is, say with the timer code, I might want to call that outside the kernel class, so I made that a global variable instead...
So I don't know whether I should have everything as global objects, or as members of the kernel object...
So, I'd like to sort this all out, re-write the base, and then port everything else I've done back to the new code. Hopefully this will be a lot less work that when I decided to write it in C++ instead.
What are other people doing to avoid this problem?
-Stephen
I have been writing a kernel in C++ for a while now (I started months ago, but have only been working on it in very short bursts)... But I'm not sure if I'm structuring the kernel the best way possible, so I'd like to ask what is the best way to do it.
Basically, the way I'm doing it now is that I have a kernel class, that has instances of the GDT, IDT etc. class etc. as members. The main function makes an instance of the kernel object, which makes the other instances in its constructor. But the problem is, say with the timer code, I might want to call that outside the kernel class, so I made that a global variable instead...
So I don't know whether I should have everything as global objects, or as members of the kernel object...
So, I'd like to sort this all out, re-write the base, and then port everything else I've done back to the new code. Hopefully this will be a lot less work that when I decided to write it in C++ instead.
What are other people doing to avoid this problem?
-Stephen
I like to use the singleton pattern, too. Means I have a static member function 'instance' that gets me the class' instance (e.g. gdt::instance() would get me the instance of the gdt class).
Your method is really bad, because it is completely unportable: You would need to use ifdefs in the kernel class definition to get the subclasses for a specific architecture in. It's also not really easily extensible.
Your method is really bad, because it is completely unportable: You would need to use ifdefs in the kernel class definition to get the subclasses for a specific architecture in. It's also not really easily extensible.
- Steve the Pirate
- Member
- Posts: 152
- Joined: Fri Dec 15, 2006 7:01 am
- Location: Brisbane, Australia
- Contact:
Re: C++ kernel design questions
You can have them as members of the Kernel object (or class), but I think there is a larger problem with your design. Kernel sounds like it is an overall god class that contains alot of members that should be inside of other interfaces, not handled by the kernel.
For example, I personally separate processor dependent interfaces from the "Core" class, so the core does not need to manage the GDT and IDT directly. After all, it should not need to as they are processor tables, not kernel defined tables.
I personally cannot help you with a new design as I do not know your requirements, though. If you provide more information, I might be able to help.
For example, I personally separate processor dependent interfaces from the "Core" class, so the core does not need to manage the GDT and IDT directly. After all, it should not need to as they are processor tables, not kernel defined tables.
I personally cannot help you with a new design as I do not know your requirements, though. If you provide more information, I might be able to help.
OS Development Series | Wiki | os | ncc
char c[2]={"\x90\xC3"};int main(){void(*f)()=(void(__cdecl*)(void))(void*)&c;f();}
char c[2]={"\x90\xC3"};int main(){void(*f)()=(void(__cdecl*)(void))(void*)&c;f();}
- Steve the Pirate
- Member
- Posts: 152
- Joined: Fri Dec 15, 2006 7:01 am
- Location: Brisbane, Australia
- Contact:
This sounds like a better way of doing it that what I'm doing.cyr1x wrote:I replace classes which have only one instance with namespaces.
I would like to separate them, but I'm not quite sure what is the best way to support multiple architectures. Obviously it's not just as simple as linking a different set of processor specific functions to the kernel, because one architecture may not support features (like a GDT or something) that another does. So do you have a separate function (with each architecture) that initialises the hardware specific code, and then decide which one to call with an ifdef or something?neon wrote:For example, I personally separate processor dependent interfaces from the "Core" class, so the core does not need to manage the GDT and IDT directly. After all, it should not need to as they are processor tables, not kernel defined tables.
Kind of. I decide what my kernel needs, and provide a standard interface that all defined hardware abstraction layers must follow. We can define architecture specific routines in the core interface as well, so long as other HALs define it (Returns error, perhaps?)Steve the Pirate wrote:So do you have a separate function (with each architecture) that initialises the hardware specific code, and then decide which one to call with an ifdef or something?
Then I just define different hardware abstraction layers for different architectures. The only thing that stays the same is the interface, which are linked either statically or loaded and used during runtime by the kernel (Similar to Windows hal.dll)
...This is the way I do it, anyways. Alot of the hardware dependent code does not ever need to be touched by the kernel. By the kernels level of abstraction, it simply tells the HAL what it needs to do in order for the kernel to complete its operations.
OS Development Series | Wiki | os | ncc
char c[2]={"\x90\xC3"};int main(){void(*f)()=(void(__cdecl*)(void))(void*)&c;f();}
char c[2]={"\x90\xC3"};int main(){void(*f)()=(void(__cdecl*)(void))(void*)&c;f();}
I have a separate 'arch' namespace (and directory structure). There is one 'arch.h' header which defines everything which needs to be provided by an architecture.
This is evolving at the moment, but currently has, for example, arch::initialise(), which is called at the start of the kernel's main routine. After this has been called, other kernel objects assume that they can use high level memory allocation (the page frame allocator and PFE handler must be online, for example).
The scheduler is an architecture independent class but is, of course, called by an interrupt from the archetecture dependent function. In the same way, the scheduler.CreateThread() function must call an arch::CreateThreadSpace() function.
So if a third party wants to port my kernel to a new architecture, all they need to do is make sure there is an implementation of each function in arch.h. These functions are being kept to a minimum.
Hope that helps a bit - and bear in mind there are lots of ways to go about this!
Cheers,
Adam
This is evolving at the moment, but currently has, for example, arch::initialise(), which is called at the start of the kernel's main routine. After this has been called, other kernel objects assume that they can use high level memory allocation (the page frame allocator and PFE handler must be online, for example).
The scheduler is an architecture independent class but is, of course, called by an interrupt from the archetecture dependent function. In the same way, the scheduler.CreateThread() function must call an arch::CreateThreadSpace() function.
So if a third party wants to port my kernel to a new architecture, all they need to do is make sure there is an implementation of each function in arch.h. These functions are being kept to a minimum.
Hope that helps a bit - and bear in mind there are lots of ways to go about this!
Cheers,
Adam
- Steve the Pirate
- Member
- Posts: 152
- Joined: Fri Dec 15, 2006 7:01 am
- Location: Brisbane, Australia
- Contact:
Yes. Here's my horribly complex makefile:
So basically, I have arch/x86_64, arch/i586 and arch/pc. This means that for the target x86_64-pc-caracal, sources are included from the /x86_64 and the /pc directories. All code which is common to x86_64 and i586 only needs to be written once and is contained in the /pc directory.
I know I'll probably receieve loads of Makefile corrections now, but it works for me! To compile for i586, I do:
My default architecture is x86_64, and that's what compiles if I define no environment variables. The arch.h header is common to all architectures.
Cheers,
Adam
Code: Select all
# Makefile for Caracal Kernel
# Thanks to Solar for his tips on the OSDev.org wiki.
# Ensure Target and Prefix variables are not blank
ifeq ($(strip $(TARGET)),)
TARGET:=x86_64
endif
ifeq ($(strip $(SHAREDTARGET)),)
SHAREDTARGET:=pc
endif
ifeq ($(strip $(SHAREDTARGET)),none)
COMBINEDTARGET:=$(TARGET)
else
COMBINEDTARGET:=$(TARGET)-$(SHAREDTARGET)
endif
ifeq ($(strip $(PREFIX)),)
PREFIX:=b:/
endif
ifeq ($(strip $(BUILD)),)
BUILD:=bin/$(COMBINEDTARGET)/
endif
ifeq ($(strip $(BINARY)),)
BINARY:=kernel.sys
endif
# Files and Paths Included in this Project
SRCBASE:= source/
ARCHBASE:= $(SRCBASE)/arch/$(TARGET)/
SHAREDARCHBASE:= $(SRCBASE)/arch/$(SHAREDTARGET)/
TARFILE:= $(patsubst %.sys, %.tar.gz, $(BINARY))
SRCDIRS:= $(shell find $(SRCBASE) -maxdepth 4 -not -name "*.*" -not -path "*arch*" -not -path "*include*") \
$(shell find $(ARCHBASE) -maxdepth 3 -not -name "*.*" -not -path "*include*") \
$(shell find $(SHAREDARCHBASE) -maxdepth 3 -not -name "*.*" -not -path "*include*")
BINDIRS:= $(patsubst $(SRCBASE)%, $(BUILD)%, $(SRCDIRS))
ASMSRC:= $(shell find $(SRCBASE) -mindepth 1 -maxdepth 4 -name "*.asm" -not -path "*arch*" -not -path "*include*") \
$(shell find $(ARCHBASE) -maxdepth 3 -name "*.asm" -not -path "*include*") \
$(shell find $(SHAREDARCHBASE) -maxdepth 3 -name "*.asm" -not -path "*include*")
CXXSRC:= $(shell find $(SRCBASE) -mindepth 1 -maxdepth 4 -name "*.cpp" -not -path "*arch*" -not -path "*include*") \
$(shell find $(ARCHBASE) -maxdepth 3 -name "*.cpp" -not -path "*include*") \
$(shell find $(SHAREDARCHBASE) -maxdepth 3 -name "*.cpp" -not -path "*include*")
ALLSRC:= $(CXXSRC) $(ASMSRC)
HDR:= $(shell find include -mindepth 1 -maxdepth 4 -name "*.h")
OBJ:= $(patsubst $(SRCBASE)%.cpp,$(BUILD)%.o,$(CXXSRC)) $(patsubst $(SRCBASE)%.asm,$(BUILD)%.xo,$(ASMSRC))
DEP:= $(patsubst $(SRCBASE)%.o,$(BUILD)%.d,$(CXXSRC))
ALL:= $(ALLSRC) $(HDR)
# General defines
CXX:= $(COMBINEDTARGET)-caracal-g++
LD:= $(COMBINEDTARGET)-caracal-ld
CXXFLAGS:= -Wall -Wextra -pedantic -Wshadow -Wpointer-arith -Wcast-align \
-Wwrite-strings -Wredundant-decls -Winline -Wno-long-long \
-Wconversion -Werror -O -fstrength-reduce -nostartfiles \
-D __KARCH_NAME=\"$(COMBINEDTARGET)\" -D __DEBUG -fno-exceptions -fno-rtti \
-fPIC -Iinclude/ -I$(ARCHBASE)/include -I$(SHAREDARCHBASE)/include
LDFLAGS:=-T $(ARCHBASE)link.ld -nostdinc -nostdlib -o
# Main Target List
.PHONY: all clean install reinstall rebuild dist distclean dump dumpall makedirs vars
all: makedirs $(BUILD)$(BINARY)
info:
-@$(COMBINEDTARGET)-caracal-objdump -x $(BUILD)$(BINARY) > $(BUILD)$(BINARY).txt
dist:
@tar -czf $(TARFILE) $(ALL)
distclean:
@rm -f $(TARFILE)
clean:
@rm -rf $(BUILD)
rebuild: clean all
install: all
-@cp $(BUILD)*.sys b:/
reinstall: clean install
vars:
@echo TARGET = $(COMBINEDTARGET)
@echo PREFIX = $(PREFIX)
@echo BUILD = $(BUILD)
@echo BINARY = $(BINARY)
-include $(DEPS)
# File Targets
$(BUILD)$(BINARY): $(OBJ)
-@$(LD) $(LDFLAGS) $(BUILD)$(BINARY) $(OBJ)
$(BUILD)%.o: $(SRCBASE)%.cpp Makefile
-@$(CXX) $(CXXFLAGS) -MMD -MP -MT "$*.d" -o $@ -c $<
ifeq ($(strip $(COMBINEDTARGET)),i586-pc)
$(BUILD)%.xo: $(SRCBASE)%.asm
-@nasm -f elf32 $< -o $@
else
$(BUILD)%.xo: $(SRCBASE)%.asm
-@nasm -f elf64 $< -o $@
endif
# 'Utility Targets'
makedirs:
@mkdir -p $(BINDIRS)
dump:
-@$(COMBINEDTARGET)-caracal-objdump -x -d $(BUILD)$(BINARY) > $(BUILD)$(BINARY).txt
I know I'll probably receieve loads of Makefile corrections now, but it works for me! To compile for i586, I do:
Code: Select all
export TARGET=i586
make install
Cheers,
Adam
-
- Member
- Posts: 2566
- Joined: Sun Jan 14, 2007 9:15 pm
- Libera.chat IRC: miselin
- Location: Sydney, Australia (I come from a land down under!)
- Contact:
I personally find it's easier to use defines for architectures and based on those choose different source directories and files.Steve the Pirate wrote:That does clear a lot up, but one thing I'm not sure about is how you pick the right arch to use. Do you only have one arch.h (ie. not a distinct one for each architecture), and then the right source files compiled linked in at runtime?
An example would be:
Code: Select all
# common kernel stuff
SRCFILES = file1.c file2.c ....
# x86 stuff
ifdef ARCH_X86
SRCFILES += arch/x86/gdt.c ....
endif
# ARM stuff
ifdef ARCH_ARM
SRCFILES += arch/arm/init.c ....
endif
Code: Select all
make all ARCH_ARM=1
- Steve the Pirate
- Member
- Posts: 152
- Joined: Fri Dec 15, 2006 7:01 am
- Location: Brisbane, Australia
- Contact: