Might you or someone else have a resource on learning how to do this?
[1] - https://old.reddit.com/r/embedded/comments/ilgyun/would_you_...
A resource you'll find immensely useful is ctyme's interrupt jump table[2][3], which is easier than reading Ralf Brown's ASCII formatted notes[4]. Of course there are easier ways to do it, but printf() debugging (or the assembly equivalent) is still by far the easiest to set up.
If you want to go write code for modern systems -- UEFI code, you can find a tutorial here[5] (I didn't write this, however, so I don't know how good it is, I can't find the resources I used :c), and the EFI spec here[6]
[1]: https://wiki.osdev.org/Main_Page
[1.5]: https://web.archive.org/web/20200901014356/https://wiki.osde...
[2]: http://www.ctyme.com/intr/int.htm
[3]: http://www.ctyme.com/rbrown.htm
[4]: http://www.cs.cmu.edu/~ralf/files.html
[5]: https://github.com/safayetahmedatge/efitutorial
[6]: https://uefi.org/sites/default/files/resources/UEFI%20Spec%2...
Here's a random bootloader init code I wrote a while ago for an ARM SoC (probably heavily inspired by uboot):
https://gist.github.com/simias/c31ae83127c77519621edfb76d599...
The vectors are a table that must be placed in a specific location in memory and are used by the CPU to know what code to execute when certain events occur. Here I only fill the "reset" entry to call my init code when the CPU boots up.
Everything in my "reset" function up to line 93 is low level ARM things: making sure the CPU is in a well defined mode, disable IRQs, initialize the cache (optional, but good for perfs) etc... All that stuff is usually described in the docs for the CPU/SoC/Controller. Note that this SoC is fairly complex, if you used a smaller microcontroller it's usually much more straightforward.
Then after that you have a loop to clear the BSS (the _bss_start and _bss_end are generated automatically by the linker script, normally when you load an executable the operating system clears those regions for you, but obviously here we're on our own).
Then I set the stack pointer register SP to _stack_base which is also defined in the linker script to point at some memory location (typically the top of the RAM). And then we're good to go, we can do the rest in C with only small bits of ASM here and there for special instructions.
I think the best way to learn that stuff is by doing, get some ARM chip with an u-boot loader and dig into it, break it, see what code does etc... Obviously you need a good knowledge of C to begin with, and at least a basic understanding of assembly, so start with that if you're not there yet.
Also: run global initializers. Getting kinda fuzzy on the boundaries, but I'd also add in the process to copy the initial contents of the .data segment from read-only to read-write storage.
Segfaults are not something C is aware of, it's usually the consequence of triggering an undefined behaviour. But UB being... undefined, all sort of things can and do happen, for instance a buffer overflow can segfault, but it can also overwrite some other variable, or the return address of the function, or do nothing at all.
That's really the problem with C. If UB meant segfault, then it wouldn't be so bad because at least you'd immediately know that something went wrong. Instead it's very easy to have small issues go unnoticed for a long time, until your application finally crashes in a seemingly unrelated location or, worse, some clever guy or gal manages to exploit the flaw for fun and profits.