C++ kernel design questions

Discussions on more advanced topics such as monolithic vs micro-kernels, transactional memory models, and paging vs segmentation should go here. Use this forum to expand and improve the wiki!
Post Reply
User avatar
Steve the Pirate
Member
Member
Posts: 152
Joined: Fri Dec 15, 2006 7:01 am
Location: Brisbane, Australia
Contact:

C++ kernel design questions

Post by Steve the Pirate »

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
My Site | My Blog
Symmetry - My operating system.
cyr1x
Member
Member
Posts: 207
Joined: Tue Aug 21, 2007 1:41 am
Location: Germany

Post by cyr1x »

I replace classes which have only one instance with namespaces.
User avatar
bluecode
Member
Member
Posts: 202
Joined: Wed Nov 17, 2004 12:00 am
Location: Germany
Contact:

Post by bluecode »

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.
User avatar
Steve the Pirate
Member
Member
Posts: 152
Joined: Fri Dec 15, 2006 7:01 am
Location: Brisbane, Australia
Contact:

Post by Steve the Pirate »

Thanks for the advice - I'll look into that.
bluecode wrote:Your method is really bad,
I thought so, and I'm glad I asked...
My Site | My Blog
Symmetry - My operating system.
User avatar
AJ
Member
Member
Posts: 2646
Joined: Sun Oct 22, 2006 7:01 am
Location: Devon, UK
Contact:

Post by AJ »

Hi,

This is a mistake I initially made with C++ too: "Woohoo - I have classes! let's make everything a class!".

Using C++ should make your life easier - if it's not appropriate to make something an object, don't.

Cheers,
Adam
User avatar
neon
Member
Member
Posts: 1567
Joined: Sun Feb 18, 2007 7:28 pm
Contact:

Re: C++ kernel design questions

Post by neon »

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.
OS Development Series | Wiki | os | ncc
char c[2]={"\x90\xC3"};int main(){void(*f)()=(void(__cdecl*)(void))(void*)&c;f();}
User avatar
Steve the Pirate
Member
Member
Posts: 152
Joined: Fri Dec 15, 2006 7:01 am
Location: Brisbane, Australia
Contact:

Post by Steve the Pirate »

cyr1x wrote:I replace classes which have only one instance with namespaces.
This sounds like a better way of doing it that what I'm doing.
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.
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?
My Site | My Blog
Symmetry - My operating system.
User avatar
neon
Member
Member
Posts: 1567
Joined: Sun Feb 18, 2007 7:28 pm
Contact:

Post by neon »

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?
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?)

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();}
User avatar
AJ
Member
Member
Posts: 2646
Joined: Sun Oct 22, 2006 7:01 am
Location: Devon, UK
Contact:

Post by AJ »

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
User avatar
Steve the Pirate
Member
Member
Posts: 152
Joined: Fri Dec 15, 2006 7:01 am
Location: Brisbane, Australia
Contact:

Post by Steve the Pirate »

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?
My Site | My Blog
Symmetry - My operating system.
User avatar
AJ
Member
Member
Posts: 2646
Joined: Sun Oct 22, 2006 7:01 am
Location: Devon, UK
Contact:

Post by AJ »

Yes. Here's my horribly complex makefile:

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
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:

Code: Select all

export TARGET=i586
make install
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
pcmattman
Member
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:

Post by pcmattman »

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?
I personally find it's easier to use defines for architectures and based on those choose different source directories and files.

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
You'd also have to set CC/AS/LD to a valid cross compiler for the target. Then all people have to do is:

Code: Select all

make all ARCH_ARM=1
Or similar.

;)
User avatar
Steve the Pirate
Member
Member
Posts: 152
Joined: Fri Dec 15, 2006 7:01 am
Location: Brisbane, Australia
Contact:

Post by Steve the Pirate »

Sounds good - thanks for that.
My Site | My Blog
Symmetry - My operating system.
Post Reply