Live data from Hacker News

The Journey Before main()

amit.prasad.me

61–70 of 145 posts

Re: The Journey Before main()

#61
post #37

Earlier quoted context omitted.

Not exactly the same, but on Windows if you use entirely Win32 calls you can avoid linking any C runtime library. Win32 is below the C standard library on Windows and the C runtime is optional.

This is one of the cornerstones that guarantee Windows can easily upgrade the C runtime and make performance and security upgrades. Win32 APIs have a different function calling ABI too. So only part of that gets "bloated" is Win32 API itself (which is spread across multiple DLLs and don't actually bloat RAM usage). Most of the time even those functions and structures are carefully designed to have some future-proofne…

I'm sick of glibc compatibility problems. Are there any recommended replacements?

Re: The Journey Before main()

#62
post #46
post #22

> The ELF file contains a dynamic section which tells the kernel which shared libraries to load, and another section which tells the kernel to dynamically “relocate” pointers to those functions, so everything checks out. This is not how dynamic linking works on GNU/Linux. The kernel processes the program headers for the main program (mapping the PT_LOAD segments, without relocating them) and notices the PT_INTERP pro…

I've always wondered why there weren't more popular loaders to choose from given that on Linux loaders are user-space

Part of it is the Glibc loader’s carnal knowledge of Glibc proper; there’s essentially no module boundary there. (That’s not completely unjustified, but Glibc is especially hostile there, like in its many other architectural choices.) Musl outright merges the two into a single binary. So if you want to do a loader then you’re also doing a libc.

Part of it for desktop Linux specifically is that a lot of the graphics stack is very unfriendly to alternative libcs or loaders. For example, Wayland is nominally a protocol admitting multiple implementations, but if you want to not be dumb[1] and do GPU-accelerated graphics, then the ABI ties you to libwayland.so specifically (event-loop opinions and all) in order to load vendor-specific userspace drivers, which entails your distro’s preferred libc (probably Glibc).

[1] There can of course be good engineering reasons to be dumb.

Re: The Journey Before main()

#63

As someone who teaches this stuff at university, I see students getting confused every single year by how textbooks draw memory. The problem is mostly visual, not conceptual. Most diagrams in books and slides use an old hardware-centric convention: they draw higher addresses at the top of the page and lower addresses at the bottom. People sometimes justify this with an analogy like “floors in a building go up,” so ad…

The stack does grow down though no matter what, in the sense that the pushing decrements the stack pointer. You can represent this as "up" in your diagram, but I don't think this makes it any easier conceptually because by analogy to a simple push/pop on an array, you'd naively expect higher addresses to contain more recent stack contents.

The core of the issue is that the direction stack growth differs from "usual" memory access patterns which usually allocate from lower to higher addresses (consider array access, or how strings are laid out in memory. And little-endian systems are the majority)

But if we're going with visualization options I prefer to visualize it horizontally, with lower addresses on left. This has a natural correspondence with how you access an array or lay out strings in memory.

Re: The Journey Before main()

#64
Hacking this stuff is so fun!!

> Depending on your program, _start may be the only thing between the entrypoint and your main function

I once developed a liblinux project entirely built around this idea.

I wanted to get rid of libc and all of its initialization, complexity and global state. The C library is so complex it has a primitive form of package management built into it:

https://blogs.oracle.com/solaris/post/init-and-fini-processi...

So I made _start functions which did nothing but pass argc, argv, envp and auxv to the actual main function:

https://github.com/matheusmoreira/liblinux/blob/master/start...

https://github.com/matheusmoreira/liblinux/blob/master/start...

You can get surprisingly far with just this, and it's actually possible to understand what's going on. Biggest pain point was the lack of C library utility functions like number/string conversion. I simply wrote my own.

https://github.com/matheusmoreira/liblinux/tree/master/examp...

Linux is the only operating system that lets us do this. In other systems, the C library is part of the kernel interface. Bypassing it like this can and does break things. Go developers once discovered this the hard way.

https://www.matheusmoreira.com/articles/linux-system-calls

The kernel has their own nolibc infrastructure now, no doubt much better than my project.

https://github.com/torvalds/linux/tree/master/tools/include/...

I encourage everyone to use it.

Note also that _start is an arbitrary symbol. The name is not special at all. It's just some linker default. The ELF header contains a pointer to the entry point, not a symbol. Feel free to choose a nice name!

Re: The Journey Before main()

#65
post #22

> The ELF file contains a dynamic section which tells the kernel which shared libraries to load, and another section which tells the kernel to dynamically “relocate” pointers to those functions, so everything checks out. This is not how dynamic linking works on GNU/Linux. The kernel processes the program headers for the main program (mapping the PT_LOAD segments, without relocating them) and notices the PT_INTERP pro…

Yeah it turns out the kernel doesn't care about sections at all. It only ever cares about the PT_LOAD segments in the program header table, which is essentially a table of arguments for the mmap system call. Sections are just dynamic linker metadata and are never covered by PT_LOAD segments.

This seems to be a common misconception. I too suffered from it once... Tried to embed arbitrary files into ELF files using objcopy. The tool could easily create new sections with the file contents just fine, but the kernel wouldn't load them into memory. It was really confusing at first.

https://stackoverflow.com/q/77468641

There were no tools for patching the program header table, I ended up making them! The mold linker even added a feature just to make this patching easy!

https://www.matheusmoreira.com/articles/self-contained-lone-...

Re: The Journey Before main()

#66

I wonder how many C projects prefer to avoid standard library, just invoking Linux syscalls directly. Much more fun to write software this way, IMO.

I once wrote a liblinux project just for this!! It was indeed extremely fun. Details in my other comment:

https://news.ycombinator.com/item?id=45709141

I abandoned it because Linux itself now has a rich set of nolibc headers.

Now I'm working on a whole programming language based around this concept. A freestanding lisp interpreter targeting Linux directly with builtin system call support. The idea is to complete the interpreter and then write the standard library and Linux user space in lisp using the system calls.

It's been an amazing journey. It's incredible how far one can take this.

Re: The Journey Before main()

#67
post #38
post #27

Earlier quoted context omitted.

File descriptors are part of the linux syscall API, not libc. Are you thinking of FILE?

The "syscall API" is part of libc too. The read syscall is a trap, you put arguments in the right registers and issue the correct instruction[1] to enter the kernel. That's not something that can be expressed in C. The read() function that your C code actually uses is a C function provided by the C library. [1] "svc 0" on ARM, "int 0x80" on i386, etc...

> That's not something that can be expressed in C.

I've often made the argument that compilers should add builtins for Linux system calls. Just emit code in the right calling convention and the system call instruction, and return the result. Even high level dynamic languages could have their JIT compilers generate this code.

I actually tried to hack a linux_system_call builtin into GCC at some point. Lost that work in a hard drive crash, sadly. The maintainers didn't seem too convinced in the mailing list so I didn't bother rewriting it.

> The read() function that your C code actually uses is a C function provided by the C library.

These are just magic wrapper functions. The actual Linux system call entry point is language agnostic, specified at the instruction architecture level, and is considered stable.

https://www.matheusmoreira.com/articles/linux-system-calls

This is different from other systems which force people to use the C library to interface with the kernel.

One of the most annoying things in the Linux manuals is they conflate the glibc wrappers with the actual system calls in Linux. The C library does a lot more than just wrap these things, they dynamically choose the best variants and even implement cancellation/interruption mechanisms. Separating the Linux behavior from libc behavior can be difficult, and in my experience requires reading kernel source code.

Re: The Journey Before main()

#68
post #37

Earlier quoted context omitted.

This is one of the cornerstones that guarantee Windows can easily upgrade the C runtime and make performance and security upgrades. Win32 APIs have a different function calling ABI too. So only part of that gets "bloated" is Win32 API itself (which is spread across multiple DLLs and don't actually bloat RAM usage). Most of the time even those functions and structures are carefully designed to have some future-proofne…

I'm sick of glibc compatibility problems. Are there any recommended replacements?

.interp to a glibc/libc you ship or static linking. These days it’s probably faster (in dev time) to just run a container than setting up a bespoke interp and a parallel set of libraries (and the associated toolchain changes or binary patching needed to support it).

Re: The Journey Before main()

#69
post #26

Earlier quoted context omitted.

Obviously only a requirement if you intend your software to run under windows. But if you don't, why bother. Not all software is intended to be distributed to users far and wide. Some of it is just for yourself, and some of it will only ever run on linux servers.

> some of it will only ever run on linux servers. I’ve spent quite a lot of time dealing with code that will ever run on Linux which did not in fact only ever run on Linux! Obviously for hobby projects anyone can do what they want. But adult projects should support Windows imho and consider Windows support from the start. Cross-platform is super easy unless you choose to make it hard.

> But adult projects should support Windows imho and consider Windows support from the start.

Hope whatever "adult" is working on the project this is getting paid handsomely. They'd certainly need to pay me big bucks to care about Windows support.

In any case, Linux system call ABI is becoming a lingua franca of systems programming. BSDs have implemented Linux system calls. Windows has straight up included Linux in the system. It looks like simply targeting Linux can easily result in a binary that actually does run anywhere.

Re: The Journey Before main()

#70
post #23

Earlier quoted context omitted.

You had me with “avoid C standard library” but lost me at “incoming Linux syscalls directly”. Windows support is a requirement, and no WSL2 doesn’t count. C standard library is pretty bad and it’d be great if not using it was a little easier and more common.

You can make CRT-free Win32 programs, read this guide[1] and you're all set. I've written a couple CLI utilities which are completely CRT-free and weigh just under a few kilobytes. [1]: https://nullprogram.com/blog/2023/02/15/

Almost freestanding. It still requires you to link against kernel32 and use the functions it provides. This is because issuing system calls directly to the Windows kernel is not supported. The kernel developers reserve the right to change things like system call numbers, so they can't be hardcoded into the application.
Post reply on HN