About Me
Welcome to my blog! This is where I collect my observations and notes on programming and technology. The main subjects range from implementation details to broader ideas about programming.
Main Topics
- Engineering Projects: Exploring implementation details and how technical systems work.
- C/C++: Notes on language features and programming techniques.
- The Programmer’s Perspective: Ideas about developing a career and a way of thinking as a programmer.
For more, visit the categories page.
Contact
If you have questions or would like to discuss something, please get in touch through the About page.
Thank you for reading and for your support. I hope these notes help you on your own technical journey!
Overview
The user program enters through _start in libos, reaches main through call_main, and exits through a syscall. The build packages the executable in a ramdisk and records its filesystem metadata. A loader copies its loadable segments into memory, and AM’s trap interface connects user syscalls to the OS event handler.
The User Program’s Entry
In this configuration, the position-dependent program is linked around 0x83000000. The startup assembly transfers control from _start to call_main, which invokes main. When main returns, the startup path calls exit.
The build places the executable in ramdisk.img, with the corresponding file information made available through files.h.
Loading Through the Filesystem
init_proc calls naive_uload with a filename such as /bin/bmp-test. naive_uload asks loader to load the program, then transfers control to the returned entry address.
The filesystem-aware loader delegates a single image to do_load_file. Rather than rely on hard-coded segment addresses, this function reads the ELF file’s own offsets, sizes, and destinations. Supporting several programs still requires a policy that prevents their loaded memory regions from colliding.
Reading an ELF32 Header
The experimental loader opens the file with fs_open, then uses fs_lseek and reads to inspect the ELF32 fields:
- At offset 16, a two-byte
e_typevalue of 2 identifies an executable of the expected kind. - At offset 24, four bytes give the entry address.
- At offset 28, four bytes give the program-header-table offset.
- At offsets 42 and 44, two-byte fields give the program-header entry size and count.
It loops over program headers and selects PT_LOAD entries. Each ELF32 entry supplies the file offset, target address, file-backed size, and in-memory size. The loader reads the file-backed bytes directly into the destination and zeroes the remaining memory with memset.
Those offsets assume the expected ELF class and byte order. A robust loader must validate the header, ranges, sizes, and read results before using them.
Dispatching System Calls
After ecall, the AM trap path first identifies the kind of event. In this runtime convention, a7 distinguishes a yield request from a syscall number once the trap cause has established the relevant environment-call case.
cte_init registers Nanos’s do_event callback. This is the connection between AM’s lower-level trap handling and the OS services implemented above it.
do_event handles the event category. Syscall events go to do_syscall, which selects the particular service using the saved syscall number. Yield handling depends on the current stage of the scheduler implementation; it should not be confused with the separate exit operation.
The principle resembles other interrupt-dispatch systems: use an identifier to select a handler, while keeping the low-level trap entry distinct from the higher-level service implementation.
Filesystem Abstractions and Syscalls
Why expose so many things as files? Much of a program’s interaction with devices and storage can be expressed as moving bytes through a small set of operations. A common interface simplifies application code even when the underlying behavior differs.
Writing to stdout produces terminal output. Reading /proc/dispinfo returns display information. Writing /dev/fb updates framebuffer data. Ordinary files, streams, and device files share the interface but need not share the same implementation.
The file table records names, sizes, storage offsets, current positions, and, where appropriate, specialized read/write handlers. printf ultimately reaches a write operation on descriptor 1 through the user-library syscall path.
For SYS_write, the OS selects the appropriate handler. A device entry may provide custom behavior; a regular ramdisk file uses ramdisk_read or ramdisk_write. Unsupported reads or writes should be rejected according to the file’s contract.
Bounds must be checked at the appropriate level. The position within a file, the size of the backing disk, and any device-specific access range are different limits. Whether a file can grow is another policy question; simply removing boundary checks is not a safe general answer.
For a framebuffer file, the driver translates the file operation into the mapped device access registered by the emulator. The return value matters too: higher-level C-library wrappers can behave unexpectedly if the low-level syscall reports the wrong result.
Debugging BMP Loading
While tracking a bug in BMP_Load, I replaced the buffered fopen, fread, and seek operations with the lower-level open, read, and lseek calls whose behavior I understood more directly.
The problem turned out to be in disk-boundary handling. I left the lower-level implementation in place after it worked.
NDL wraps the underlying OS interfaces. A separate unresolved-symbol problem in an NDL call was solved by reading the makefile hierarchy and adding the missing NDL library to the link. At this point in the project, the rest of the NDL and miniSDL functionality had not yet been implemented.
If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. All the images used in the blog are my original works or AI works, if you want to take it,don't hesitate. Thank you !