4 ms·
Show HN: A toy bootloader written in C++
Yes, this includes the MBR and partition boot sector. The partition table is modeled as a class hierarchy. Second stage loads a kernel from a FAT32 file.
I'm normally a C programmer, but for a work project, I recently revisited C++, a language that I had an on-again-off-again relationship over the years.
I did this as a fun training exercise on the side to get familiar with the language again, but obsessed a bit and kind of overshot the target. I thought the result might be of interest to someone.
- leetrout 4y agoWhen talking about a compiler course >>> To a degree, it changed my way of thinking about programming. Source code should first and foremost be written for people to read. Source code should convey high level meaning, both to people, as well as to the compiler. Bit twiddling hacks are an utter waste of time and make the job harder for the compiler as well. Keeping down asymptotic complexity is your job, optimizing the coefficient is the compilers job (but you should really be thinking in terms of data structures anyway, rather than algorithms). Thank you for that. Sometimes I feel like we don't give enough permission to "just" be a developer and focus on writing simple, understandable code. Maybe not what you intended but it resonated with me.
- fnordpiglet 4y agoI’ve always advised juniors that code is written once, modified twice, and read a thousand times.
- dataflow 4y agoIf that's the stick we measure with, then don't forget to count how many times code is executed.
- fnordpiglet 4y agoMy saying for that measure is “make it work, make it right, make it fast, make it small.” Those are the dimensions the machine cares about, after all the readable code is translated for the machine from the original written form.
- dataflow 4y ago> make it work, make it right, make it fast, make it small I'm not entirely sure I understand the difference between the first two (?) given that if something is working wrong, then I would call it broken, not working. But these are a bit beside the point I was trying to make. > after all the readable code is translated Prioritizing readability before all of those breaks your own yardstick though. Your metric for prioritization was the frequency of each event, and it seems you just changed your metric when it didn't give the outcome you liked! Note I'm not taking a stance on the priorities here, just pointing out that the argument you put forward only looks convincing when you conveniently omit the half of the story about execution. The moment the listener thinks "but my code will execute several orders of magnitude more frequently than it will be read by a human", it breaks down. Execution is arguably the more important half too, since horribly unmaintainable code that runs can still deliver some value, whereas perfect code that doesn't run doesn't deliver any value! I imagine probably a better yardstick is the cost/benefit trade-off of the code in the long run - trying to estimate SWE-minutes it costs in the future vs. how much money it would save in execution.
- asddubs 4y agoThe difference between the first two is how ugly the code is
- fnordpiglet 4y agoYeah make it work means don’t spend time figuring out how to do it right - just make it work first. Then you’ll usually know exactly how to make it right. Once it’s made right it’ll be more obvious how to make it fast. Once it’s fast, you’ll see the fat left over and you can trim it down. The benefit of this approach is you can choose when to stop - often working is enough, and these days things rarely reach small.
- mike_hock 4y agoUntil you profile and find out that the compiler didn't properly optimize and you replace it with a bit hack. You can't look at a particular piece of code in isolation and judge whether it was optimized prematurely.
- tialaramex 4y agoIf the compiler mis-optimises this... foo.bar(baz); ... in a way which has unacceptable performance don't just replace it with inexplicable nonsense that you happen to know does the right thing in this particular compiler on this particular day for this particular architecture... let tmp = baz << 2; foo |= (tmp ^ 0x34F2A7); ... instead the correct thing is something like this: inline function compiler_hack(foo, bar) { let tmp = bar << 2; foo |= (tmp ^ 0x34F2A7); } // FooCompiler 11.65 mis-optimises foo.bar(baz); for the Axilon 480 compiler_hack(foo, bar);
- ngneer 4y agoWow, very nice work!
- gjvc 4y agoGreat job! This will be of much help to people writing toy OSes :-)
- todd8 4y agoI once wrote a Boot loader for a minicomputer that had a card reader. One card holds 80 chars and a physical buttons on the machine would load those bytes into low memory and set the program counter to the address of the first byte. I could just squeeze in enough binary machine instructions that loaded 6 more cards of machine code that made up the rest of the boot loader. That way, a user could put these 7 cards on the front of any program they wanted to run and put it all into the card reader. Things were actually a bit more complex, a FORTRAN program would require the compiler to be next in the stack followed by the user’s FORTRAN code and then the data that would be data read by the program. The goal of this reading one card that would read more cards was to minimize the amount of switches that had to be set on the front panel to kick off the whole sequence. One card couldn’t even hold the final branch for the machine language loop that read cards 2 through 7. That branch instruction was at the front of card 2 that has been read into into memory by the body of the loop. TLDR: lots of bit twiddling was needed to create an ergonomic loader.
- colejohnson66 4y agoReminds me of the IBM 1401 where the boot card needs to not only load the next card, but place "word marks" on the read "characters". Only after a few cards would you have bootstrapped enough to go further. Ken Shirriff did a post about it a few years ago: http://www.righto.com/2021/02/an-ibm-1401-mainframe-computer-at.html http://www.righto.com/2021/02/an-ibm-1401-mainframe-computer...
- mtlynch 4y agoThis is cool! Thank you for sharing! I've fantasized about doing something similar. What resources did you find helpful in learning to do this? What would you estimate was your time investment to get it working?
- st_goliath 4y ago> What resources did you find helpful in learning to do this? I'd say the good old osdev wiki: https://wiki.osdev.org/Main_Page https://wiki.osdev.org/Main_Page Also, the Linux boot strapping code is a good read (arch/x86/boot), especially since this is actually used in production for at least 2 decades or so. But really, it helped a lot having fidgeted with this countless times when I was 15, being obsessed about writing a tiny OS. So I knew what had to be done and where the journey would go. > What would you estimate was your time investment to get it working? Getting the MBR and VBR to work: About a weekend (again, with the unfair advantage of having done this in hand written assembly dozens of times before). Getting the FAT code to work: A few more weekends staring at hex dumps and maybe some nights and a few cleanup/restructure phases in between. The main road blocks typically involved stumbling into one GCC or BIOS oddity after the other (the way GCC generates 16 bit code, getting the inline assembly snippets to work, BIOS touching registers it shouldn't, mangling the stack pointer, using my stack a scratch buffer...). A hand full of times, the CHS arithmetic was off, occasionally I had to choose a different approach because I ran out of space. The code doesn't actually do much and it mainly required the right amount of stubbornness to get it running.
- mtlynch 4y agoCool, thank you!