#![no_std]
#![no_main]
use core::panic::PanicInfo;
#[panic_handler]
fn panic_handler(_panic: &PanicInfo<'_>) -> ! {
// TODO: write panic message to stderr
write(2, "Panic occured\n".as_bytes()); // TODO: panic location + message
unsafe { sc::syscall!(EXIT, 255 as u32) };
loop {}
}
fn write(fd: usize, buf: &[u8]) {
unsafe {
sc::syscall!(WRITE, fd, buf.as_ptr(), buf.len());
}
}
#[no_mangle]
pub extern "C" fn main() -> u32 {
write(1, "Hello, world!\n".as_bytes());
return 0;
}
Then I inspected the ELF output in Ghidra. No matter what it was about ~16kb. I'm sure some code golf could be done to get it done (which has obviously been done + written about + documented before) rustc hello.rs -C panic=abort -C opt-level=3 -C link-arg=/entry:main
Here's the program: #![no_std]
#![no_main]
#[panic_handler]
fn panic_handler(_panic: &core::panic::PanicInfo<'_>) -> ! {
unsafe { ExitProcess(111) };
}
#[no_mangle]
pub extern "C" fn main() -> u32 {
let msg = b"Hello, world!\n";
unsafe {
let stdout = GetStdHandle(-11);
let mut written = 0;
WriteFile(
stdout,
msg.as_ptr(),
msg.len() as u32,
&mut written,
core::ptr::null_mut(),
);
}
0
}
#[link(name = "kernel32")]
extern "system" {
fn ExitProcess(uExitCode: u32) -> !;
fn GetStdHandle(nStdHandle: i32) -> isize;
fn WriteFile(
hFile: isize,
lpBuffer: *const u8,
nNumberOfBytesToWrite: u32,
lpNumberOfBytesWritten: *mut u32,
lpOverlapped: *mut (),
) -> i32;
}
I didn't bother much with the panic handler because there's no reason to in hello world. Though the binary contains a fair bit of padding still so it could have a few more things added to it without increasing the size. Alternatively I could shrink it a bit further by doing crimes but I'm not sure there's much point.It may be worth noting that the associated pdb (aka debug database) is 208,896 bytes.
rustc hello.rs -C panic=abort -C opt-level=3 -C link-args="/ENTRY:main /DEBUG:NONE /EMITPOGOPHASEINFO /EMITTOOLVERSIONINFO:NO /ALIGN:16"You can easily get around 500 bytes this way
(and if you're using a language with a stack, your executable probably ultimately loads as at least two pages: r/o and r/w)
I need roughly 24 bytes. 16kb means 16.384 bytes. I can think of better usage of $4000 space in memory, FLI for example.
The "better behaved" way is to call vDSO. It's a magic mini-library which the kernel automatically maps into your address space. Thus the kernel is free to provide you with whatever code it deems optimal for doing a system call.
In particular some of the system calls might be optimized away and not require the `syscall` at all because they are executed in the userspace. Historically you could expect vDSO to choose between different mechanisms of calling the kernel (int 0x80, sysenter).
Follow-up: https://drewdevault.com/2020/01/08/Re-Slow.html
This legendary blogpost makes the smallest Linux program (the program simply exits with status 42): https://www.muppetlabs.com/~breadbox/software/tiny/teensy.ht...
You can also find the smallest "Hello World" program on that website.
If you are interested in that argument, see https://gist.github.com/kenballus/c7eff5db56aa8e4810d39021b2....
Out of these 23 bytes, 15 bytes are consumed by the dollar-terminated string itself. So really only eight bytes of machine code that consists of four x86 instructions.
> Given a hello world C++ snippet, submit the smallest possible compiled binary
I remember using tools like readelf and objdump to inspect the program and slowly rip away layers and compiler optimizations until I ended up with the smallest possible binary that still outputted "hello world". I googled around and of course found someone who did it likely much better than any of us students could have ever managed [1][1]: https://www.muppetlabs.com/%7Ebreadbox/software/tiny/teensy....
That should be like 10 x86 instructions tops, plus the string data.
For instance, when I tried to make the smallest x86-64 Hello World program (https://tmpout.sh/3/22.html), I ended up lengthening it from 11 to 15 instructions, while shortening the ultimate file from 86 to 77 bytes.
mov ah, 9
mov dx, 108
int 21
ret
db "hello world!$"This was actually a perfect ending to this piece.
What comes after the syscall is where everything gets very interesting and very very complicated. Of course, it also becomes much harder to debug or reverse-engineer because things get very close to the hardware.
Here's a quick summary, roughly in order (I'm still glossing over; each of these steps probably has an entire battalion of software and hardware engineers from tens of different companies working on it, but I daresay it's still more detailed than other 'tours through 'hello world'):
- The kernel performs some setup setup to pipe the `stdout` of the hello world process into some input (not necessarily `stdin`; could be a function call too) of the terminal emulator process.
- The terminal emulator calls into some typeface rendering library and the GPU driver to set up a framebuffer for the new output.
- The above-mentioned typeface rendering library also interfaces with the GPU driver to convert what was so far just a one-dimensional byte buffer into a full-fledged two-dimensional raster image:
- the corresponding font outlines for each character byte is loaded from disk;
- each outline is aligned into a viewport;
- these outlines are resized, kerning and font metrics applied from the font files set by the terminal emulator;
- the GPU rasterises and anti-aliases the viewport (there are entire papers and textbooks written on these two topics alone). Rasterisation of font outlines may be done directly in hardware without shaders because nearly all outlines are quadratic Bézier splines.
- This is a new framebuffer for the terminal emulator's window, a 2D grid containing (usually) RGB bytes.
- The windowing manager takes this framebuffer result and *composits* with the window frame (minimise/maximise/close buttons, window title, etc) and the rest of the desktop—all this is done usually on the GPU as well.
- If the terminal emulator window in question has fancy transparency or 'frosted glass' effects, this composition applies those effects with shaders here.
- The resultant framebuffer is now at the full resolution and colour depth of the monitor, which is then packetised into an HDMI or DisplayPort signal by the GPU's display-out hardware, depending on which is connected.
- This is converted into an electrical signal by a DAC, and the result piped into the cable connecting the monitor/internal display, at the frequency specified by the monitor refresh rate.
- This is muddied by adaptive sync, which has to signal the monitor for a refresh instead of blindly pumping signals down the wire
- The monitor's input hardware has an ADC which re-converts the electrical signal from the cable into RGB bytes (or maybe not, and directly unwraps the HDMI/DP packets for processing into the pixel-addressing signal, I'm not a monitor hardware engineer).
- The electrical signal representing the framebuffer is converted into signals for the pixel-addressing hardware, which differs depending on the exact display type—whether LCD, OLED, plasma, or even CRT. OLED might be the most complicated since each *subpixel* needs to be *individually* addressed—for a 3840 × 2400 WRGB OLED as seen on LG OLED TVs, this is 3840 × 2400 × 4 = 36 864 000 subpixels, i.e. nearly 37 million pixels.
- The display hardware refreshes with the new signal (again, this refresh could be scan-line, like CRT, or whole-framebuffer, like LCDs, OLEDs, and plasmas), and you finally see the result.
Note that all this happens at most within the frame time of a monitor refresh, which is 16.67 ms for 60 Hz.Nice explanation but you stopped at the human's visual system which is where everything gets very interesting and very very complicated. :)
Otherwise, its gets very interesting and very very complicated. :)
However, the point of a hello world program is to introduce programming to beginners, and make a computer do something one can visibly see. I daresay this is made moot if you pipe it into /dev/null. I could then replace 'hello world' in the title with 'any program x that logs to `stdout`', and it wouldn't reach HN's front page.
In the same vein is this idea of 'a trip of abstractions'—I don't know about you, but I always found most of them very unsatisfying as they always stop at the system call, whether Linux or Windows. It really is afterwards where things get interesting, you can't deny that.
Sure you can: "tcc -run hello.c". Okay, technically that's an in-memory compiler rather than an interpreter.
For extra geek points, have your program say "Hellorld" instead of "Hello world".
I just tried and it shows me 33738 lines (744 lines for C btw).
In a language like C++, even Hello World uses like half of all the language features.
With C you just need a definition for printf instead of including stdio. In the old days you'd get by without even defining it at all.
void main() { puts("Hello, World!"); }
You get a warning, but it is fine.GCC and Clang have a `__builtin_printf()` instead, quite useful for adhoc printf-debugging without having to include stdio.h. Under the hood it just resolves to a regular printf() or puts() stdlib call though.
Similarly, in C, you can just give the definition of printf and omit the include of stdio.h, which also saves a ton of preprocessed lines.
This article is very much in that same friendly, explanatory spirit, although obviously it goes into greater depth and uses a modern system.
Thanks for the chuckle.
And I didn't even know about most of the ones in this post.
I think that's the answer to the question of "why do we have so many". It's a great thing you don't have to know about them. Go down a layer, and the people working there will think it's a great thing they don't have to worry about the abstraction below. Software development is, currently, a human task, so the human needs necessarily structure it.
I can't comment on web development...
Are in fact the things I think have become worse from the abstractions.
Well, the recent abstractions. I like the ones that were widespread until about 2018 or so.
You might save some bytes but boy how annoying and complicated it can get.
And yet, 99% of the people I've ever seen in the industry have no idea how any of the code they write works.
I used to ask a simple interview question, I wanted to see if potential hires could explain what a pointer or memory was. Few ever could.
This is the conventional wisdom, but it's increasingly not true.