15 ms·
Machine Code Isn't Scary
- oleganza 1y agoThank you Jimmy, great article. My 23+ year experience in computer science and programming is a zebra of black-or-white moments. For the most time, things are mostly obscure, complicated, dark and daunting. Until suddenly you stumble upon a person who can explain those in simple terms, focus on important bits. You then can put this new knowledge into a well-organized hierarchy in your head and suddenly become wiser and empowered. "Writing documentation", "talking at conferences", "chatting at a cooler", "writing to a blog" and all the other discussions from twitter to mailing lists - are all about trying to get some ideas and understanding from one head into another, so more people can get elucidated and build further. And oh my how hard is that. We are lucky to sometimes have enlightenment through great RTFMs.
- amelius 1y agoMachine code was only "scary" in the old days when you had to reboot your system when you made a small mistake.
- rausr 1y agoI enjoyed having to reload everything from tape (compact cassette tape - ie the kind of thing you'd use with a home computer in the early eighties), after a crash due to my poor code. I think the term used then was "character building" ;)
- chopin 1y agoOne of my first C programs wrote straight into the BIOS memory (1989 iirc). The machine froze and refused to reboot. We had to remove the BIOS battery to reset the BIOS. Luckily, the battery wasn't soldered to the main board.
- PlunderBunny 1y agoI taught myself to program on an 8-bit BBC micro-computer in the mid-80s by typing in BASIC listings. I understood BASIC quite well, and could write my own structured BASIC programs, but machine code was always a bit out-of-reach. I would try to read books that started by demonstrating how to add, subtract etc, but I couldn’t see how that could build up to more complicated stuff that I could do in BASIC, like polling for input, or playing sounds, or drawing characters on the screen. Only once I got an advanced users guide and discovered the operating system commands, then it started to click with me - the complicated stuff was just arranging all the right data in the right bits of memory or registers, then (essentially) calling a particular OS command and saying ‘here’s the data you want’.
- bowsamic 1y agoYeah the issue is that the pedagogy doesn’t make it clear how to bridge the “calculator” with the OS stuff. I had this issue when I was a kid. How does adding eventually make something draw on the screen? Of course, it doesn’t, you need some hardware or OS specific information
- eterm 1y agoIt wasn't until I read Petzold's CODE that this stuff, especially the role of the the motherboard bridging processing, memory and I/O and what an OS is for, that it started to click for me.
- bowsamic 1y agoYeah my stepdad bought me CODE as a kid and that really helped. A similar book that took a "top down" approach as much as possible would have also helped though, but would be much harder to pull off
- tux3 1y agoBut if this doesn't satisfy your curiosity, you might realize this is just pushing the magic blackbox/question mark a little further down the chain How does the OS and the hardware draw on the screen, actually? All they have is also just calculator stuff, super basic primitives. You can't even do loops in hardware, or even real branches (hardware always "executes both sides" of a branch at once) Anyways, if you keep digging long enough you eventually end up finding this XKCD https://xkcd.com/435/ https://xkcd.com/435/ =)
- shakna 1y agoI started building a Forth recently, but decided that instead of interpreter or transpiler or whatever, I'd map to bytes in memory and just straight execute them. This non-optimising JIT has been far, far easier than all the scary articles and comments I've seen led me to believe. I'm already in the middle of making it work on both Aarch64 and RISC-V, a couple weeks in.
- simpleui 1y agoVery interesting, care to share the source?
- shakna 1y agoOh, it's still a while off that. I do plan to make it public at some point, but when I'm actually happy the code isn't completely vomit. But for a simple taste, the push to stack function currently looks like this. (All the emit stuff just writes bytes into a mmap that gets executed later.) void compile_push_literal(Value val) { #if ARCH_X86_64 emit_bytes((uint8_t[]){X86_MOV_RDI_IMM64_0, X86_MOV_RDI_IMM64_1}, 2); emit_uint64_le(val); emit_bytes((uint8_t[]){X86_MOV_RAX_IMM64_0, X86_MOV_RAX_IMM64_1}, 2); emit_uint64_le((uint64_t)push); emit_bytes((uint8_t[]){X86_CALL_RAX_0, X86_CALL_RAX_1}, 2); #elif ARCH_ARM64 uint64_t imm = val; emit_uint32_le(ARM64_MOVZ_OP | (ARM64_REG_X0 << 0) | ((imm & 0xFFFF) << 5)); emit_uint32_le(ARM64_MOVK_OP_LSL16 | (ARM64_REG_X0 << 0) | (((imm >> 16) & 0xFFFF) << 5)); emit_uint32_le(ARM64_MOVK_OP_LSL32 | (ARM64_REG_X0 << 0) | (((imm >> 32) & 0xFFFF) << 5)); emit_uint32_le(ARM64_MOVK_OP_LSL48 | (ARM64_REG_X0 << 0) | (((imm >> 48) & 0xFFFF) << 5)); uint64_t func_addr = (uint64_t)push; emit_uint32_le(ARM64_MOVZ_OP | (ARM64_REG_X1 << 0) | ((func_addr & 0xFFFF) << 5)); emit_uint32_le(ARM64_MOVK_OP_LSL16 | (ARM64_REG_X1 << 0) | (((func_addr >> 16) & 0xFFFF) << 5)); emit_uint32_le(ARM64_MOVK_OP_LSL32 | (ARM64_REG_X1 << 0) | (((func_addr >> 32) & 0xFFFF) << 5)); emit_uint32_le(ARM64_MOVK_OP_LSL48 | (ARM64_REG_X1 << 0) | (((func_addr >> 48) & 0xFFFF) << 5)); emit_uint32_le(ARM64_BLR_OP | (ARM64_REG_X1 << 5)); #elif ARCH_RISCV64 emit_load_imm_riscv(val, RISCV_REG_A0, RISCV_REG_T1); emit_load_imm_riscv((uint64_t)push, RISCV_REG_T0, RISCV_REG_T1); emit_uint32_le((0 << 20) | (RISCV_REG_T0 << 15) | (RISCV_F3_JALR << 12) | (RISCV_REG_RA << 7) | RISCV_OP_JALR); #endif }
- davemp 1y agoWhen I was last working with machine code, I found capstone to be very useful. Even just reading the source was helpful for some of the conditionally present amd64 fields. https://github.com/capstone-engine/capstone https://github.com/capstone-engine/capstone
- jmclnx 1y agoThat is one thing that was nice about DOS, you where close to the machine. I never fully got machine language, but it was fun trying. IIRC, debug.com could be used to create programs using machine lang.
- pjmlp 1y agoYes, it was a quite bare bones experience when compared to TASM and MASM, and only worked for COM executable, but it did work. The reasoning being that COM files were a plain memory dump starting at offset 100H, thus you would type the code in memory and then dump it.
- dedicate 1y agoFor me the 'scary' part of machine code was never the actual logic. It was always just staring at that wall of hex or mnemonics and feeling like I needed a secret decoder ring!
- jiehong 1y agoYes, that does not help. To me, it looks like some kind of complex tetris game. I guess we could maybe represent a program as such, with pieces for registers, instructions, etc. Yet, the tooling we have is very terse, and textual.
- Lerc 1y agoI have tried to convince people that ASM is reasonable as a first stage teaching language. The reputation as a nearly mystical art practiced by a few doesn't help. The thing is, instructions are simple. Getting them to do things is not hard, the difficulty comes from tasks exceeding a scale where you can think about things at their most basic level. It quickly becomes tedious to do large programs, not really hard, just unmanagable, which is precisely it should be taught as a first language. You learn how do do simple things and you learn why programming languages are used. You teach the problem that is being solved before teaching more advanced programming concepts that solve the problem.
- flohofwoe 1y agoIMO It depends a lot on the assembly flavour. The best ISA for learning is probably the Motorola 68000, followed by some 8-bit CPUs (6502, 6809, Z80), also probably ARM1, although I never had to deal with it. I always thought that x86 assembly is ugly (no matter if Intel or AT&T). > It quickly becomes tedious to do large programs IME with modern tooling, assembly coding can be surprisingly productive. For instance I wrote a VSCode extension for 8-bit home computers [1], and dog-fooded a little demo with it [2], and that felt a lot more productive than back in the day with an on-device assembler (or even typing in machine code by numbers). [1] https://marketplace.visualstudio.com/items?itemName=floooh.vscode-kcide https://marketplace.visualstudio.com/items?itemName=floooh.v... [2] https://floooh.github.io/kcide-sample/kc854.html?file=demo.kcc https://floooh.github.io/kcide-sample/kc854.html?file=demo.k...
- whobre 1y agoI agree that M68k is nice, as are the 8-bit ones you mention. I just find it strange that you like Z80 and dislike x86 - they are fundamentally not that different and both are descended from 8080.
- flohofwoe 1y agoYeah the Z80 instruction set is quite messy (mainly because it had to fill gaps of the 8080 instruction set for backward compatibility). But as an evolution of the 8080 instruction set, the Z80 is still cleaner than x86 (IMHO!). Also, the Z8000 looks quite interesting and like the better 16-bit alternative to the x86, but it never took off: https://en.wikipedia.org/wiki/Zilog_Z8000 https://en.wikipedia.org/wiki/Zilog_Z8000
- flohofwoe 1y agoHere's a similar (and much more indepth) opcode decoding recipe for Z80, very useful for emulator development: http://www.z80.info/decoding.htm http://www.z80.info/decoding.htm For actually programming in machine code this understanding of the internal opcode structure isn't all that useful though, usually - without an assembler at hand - you had a lookup table with all possible assembly instructions on the left side, and the corresponding machine code bytes on the right side. Programming by typing machine code into a hex editor is possible, but really only recommended as absolute fallback if there's no assembler at hand - mainly because you had to keep track of all global constant and subroutine entry addresses - e.g. the main thing that an assembler does for you, and you had to leave gaps at strategic locations so that it is possible to patch the code without having to move things around.
- GuB-42 1y agoWell, machine code is scary. But like with many scary things, once you overcome your fears and get familiar with the thing, you realize that it is not that bad.
- jebarker 1y agoASM programming is fun. Machine code (as in what ASM encodes to) isn't scary, but it is extremely tedious to work with. I recommend the first part of Casey Muratori's Performance Aware Programming course if you want to feel that pain.
- ferguess_k 1y agoI think you need to do it in production to retain the knowledge. If you just do it as a hobby, then most people just give up at certain point because there is no point to bang your head on the wall for nothing. You need to have a real problem to solve to go a long way.
- elif 1y agoThis is a tangent but yesterday I was just pondering the abstraction layer from machine code to assembly lexers to compilers to interpreted languages. Having been lucky enough to be born at a time to witness these shifts, it's so easy to forget what we used to think was normal. The thought came to me when testing the new Jules agentic coding platform. It occured to me that we are just in another abstraction cycle, but like those before, it's so hard to see the shift when everything is shifting. My conclusion? There will be non-AI coders, but they will be as rare as C programmers before we realize it.
- neomech 1y agoIn 1982, I programmed my ZX81 by converting assembly to hex by hand because BASIC was just too slow. I'd write my assembly on paper, convert it to hex using reference tables, then use a simple BASIC FOR loop to POKE the values into memory we'd reserved space for the machine code in a REM statement at a fixed position in memory. When all the values were POKEd in, I'd save to tape and execute it with RAND USR 16514. That memory address is permanently etched in my brain even now. It wasn't good, bad or scary it was just what I had to do to make the programs I wanted to make.
- stevekemp 1y agoI did the same thing, on the 48k Spectrum, a year or two later. I also remembered to add some NOPs between functions, to avoid me having to recalculate all the relative jump instructions if I made changes.
- kibwen 1y ago> But what if we want to represent a number larger than 12 bits? Well, the add instruction doesn't let us represent all such numbers; but setting sh to 1 lets us shift our number by 12 bits. So for example we can represent 172032172032 by leaving our 42 alone and setting sh to 1. This is a clever technique for encoding larger numbers in a small space. This is whacky. So presumably adding with a 24-bit constant whose value isn't representable in this compressed 13-bit format would then expand to two add instructions? Or would it store a constant to a register and then use that? Or is there a different single instruction that would get used? (Also, typo, 42 << 12 is 172032).
- Joker_vD 1y agoYes, it would: add_large_const: add w8, w0, #43, lsl #12 add w0, w8, #42 ret The same happens e.g. on RISC-V: add_large_const: lui a5,43 addi a5,a5,42 add a0,a0,a5 ret Because there are only 32 bits in an instruction, you can't fit a whole 32-bit immediate into it. Contrast it with x86 which uses variable-length instructions (up to 15 bytes per instruction): add_large_const: lea eax, [rdi+176170] # this instruction takes 6 bytes ret P.S. Some people seem to be really puzzled by LEA instructions. It's intended use is to correspond to C's "dest = &base[offset]" which semantically is just "dest = base + offset", of course — but it allows one to express base and/or offset using available addressing modes.
- deleted 1y ago[deleted]
- tasty_freeze 1y agoFor the past year or so, a couple teen boys from my neighborhood come by on sunday afternoon for a couple hours of programming in python. I started very simply and built up with text based tasks, then showed them pygame. I am thinking about showing them what is under the hood, that python itself is just a program. When I learned to program it was the late 70s, and trs-80s and apple-IIs were easy to understand at the machine code level. I could recapitulate that experience for them, via an emulator, but that again just feels like an abstraction. I want them to have the bare-metal experience. But x86 is such a sprawling, complicated instruction set that it is very intimidating. Of course I'd stick to a simplified subset of the instructions, but even then, it seems like a lot more work to make output running on a PC vs on the old 8-bit machines where you write to a specific location and it shows up on the screen.
- TuringTourist 1y agoYou could try the thing that made it click for me, long after x86 was dominant. Show them a CPU running on Logisim (or the like, such as the newer Digital) and show how when you plug a program into a ROM, it turns into wires lighting up and flipping gates/activating data lines/read registers etc.
- ThrowawayR2 1y agoBuy them a copy of "Human Resource Machine" on Steam or (preferably since there's no DRM) Good Old Games. It's a gamified version of what writing machine language on the old 8 bit CPUs of yore was like. The puzzle challenge in HRM is authentic in the sense that it derives from the natural constraints of having a single accumulator and very simple instructions rather than unnaturally injected constraints like the Zachtronics games, which are good but I wouldn't recommend as a learning tool.
- redf1sh 1y agoOk, what about VLIW ASM? Have you ever seen Elbrus' ASM with predicated code, asynchronous Array Prefetch Buffer, rotating registers, DAM (hardware table to memory dependencies disambiguation), registers windows etc. It's really hard to start read this.
- addaon 1y agoI think this article is missing a major point, or perhaps should be titled "Some Non-Scary Machine Code Isn't Scary". It argues that machine code isn't scary, by building a one-to-one mapping from machine code to assembly code, and then taking it as given that assembly code isn't scary. But it uses two examples -- 32-bit ARM and x86-64 -- where this one-to-one mapping isn't valid. When in Thumb mode for (some flavors of) ARM, even when you know you're in thumb mode, instructions can be a mix of 16 and 32 bits. And in x86 world, of course, instructions can be a wide range of widths. What that means is that if you're given a chunk of memory that is known to contain executable instructions... you /can't/ build a one-to-one mapping to assembly without knowing where all of the entry points are. For well-formed code you can often exclude almost all possible entry points as invalid, and maybe even end up with only a single one... but it's perfectly possible (and quite fun) to write machine code that has valid, different behavior for different entry points to the same byte sequence. There's no way to reduce this type of machine code to meaningful assembly, and it should be considered scary.
- HeyLaughingBoy 1y agoReading this thread leaves me with the impression that most posters advocating learning assembly language have never had to use it in a production environment. It sucks! For the overwhelming majority of programmers, assembly offers absolutely no benefit. I learned (MC6809) assembly after learning BASIC. I went on to become an embedded systems programmer in an era where compilers were still pretty expensive, and I worked for a cheapskate. I wrote an untold amount of assembly for various microcontrollers over the first 10 years of my career. I honestly can't say I got any more benefit out of than programming in C; it just made everything take so much longer. I once, for a side gig, had to write a 16-bit long-division routine on a processor with only one 8-bit accumulator. That was the point at which I declared that I'd never write another assembly program. Luckily, by then gcc supported some smaller processors so I could switch to using Atmel AVR series.
- zahlman 1y ago> most posters advocating learning assembly language have never had to use it in a production environment... For the overwhelming majority of programmers, assembly offers absolutely no benefit. I don't follow. Why should assembly have to be useful or pleasant in a production environment, for learning it to be useful? I was taught a couple different flavours of assembly in university, and I found it quite useful for appreciating what the machine actually does. Certainly more so than C. Abstractions do ultimately have to be rooted in something.
- atoav 1y agoYou and the post you commented on display both a valid point. If we're talking about using assembly as a broad general purpose programming environment that would be a mess (which is precisely why it has no broad adoption). When we talk about assembly as a niché special purpose solution we would come to a different conclusion, coincidentally this is where assembly is still used today: environments where we need highly optimized code. Your point about education is orthogonal to the point made. I agree with you that learning assembly can be a good way to teach people how computers work on a low level, but that has nothing to do with whether it is useful as a skill to learn. As someone teaching similar things at the university level to a non-tech audience I have to always carefully wheigh how much "practically useless" lessons a typical art student can stomach. And which kind of lesson will just deter them, potentially forever.
- unoti 1y agoThis is the video I wished I had seen when I was a kid, feeling like assembly was a dark art that I was too dumb to be able to do. Later in life I did a ton of assembly professionally on embedded systems. But as a kid I thought I wasn’t smart enough. This idea is poison, thinking you’re not smart enough, and it ruins lives. https://youtu.be/ep7gcyrbutA?si=8HiMqH2mMwsJRNDg https://youtu.be/ep7gcyrbutA?si=8HiMqH2mMwsJRNDg
- nkapias 1y ago[dead]
- boricj 1y agoMachine code isn't scary, but its nature is severely misunderstood. Skipping over the bundling of instructions into code blocks, the next logical construct are functions. These have references to code and data in memory; if you want to relocate functions around in memory you introduce the concept of relocations to annotate these references and of a linker to fix them to a particular location. But once the linker has done its job, the function is no longer relocatable, you can't move it around... or that is what someone sane might say. If you can undo the work of the linker, you can extract relocatable functions from executables. These functions can then be reused into new executables, without decompiling them first; after all, if what you've extracted is equivalent to the original relocatable function, you can do the same things than it. Repeat this process over the entire executable and you're stripped it for parts, ready to be put back together with the linker. Change some parts and you have the ability to modify it as if you're replacing object files, instead of binary patching it in place with all the constraints that comes with it. Machine code is like Lego bricks, it just takes a rather unconventional point of view (and quite a bit of time to perfect the art of delinking) to realize it.
- kgilpin 1y agoIs it enough to play Human Resource Machine!? https://en.wikipedia.org/wiki/Human_Resource_Machine https://en.wikipedia.org/wiki/Human_Resource_Machine Assembly as a game, I loved playing it.
- unoti 1y ago> Is it enough to play Human Resource Machine!? That is fun. But this one truly is enough: Turing Complete. You start with boolean logic gates and progressively work your way up to building your own processor, create your own assembly language, and use it to do things like solve mazes and more. Super duper fun https://store.steampowered.com/app/1444480/Turing_Complete/ https://store.steampowered.com/app/1444480/Turing_Complete/
- thuanao 1y agoI think machine code and building something like a Forth is way easier to understand than any contemporary programming language toolchain.
- pillars 1y agoI can relate to you, OP.Sorry for off topic I want to share my experience about my journey of programming. When I started programming, I was introduced to the ideas of variables, conditions, loops, functions—regular programming constructs. But I didn't understand much of what was going on, how the computer worked, or anything beyond just the syntax. I used to wonder: when I start my PC, how does it even know where to begin? I often felt insecure and anxious about all these unknowns. I was trapped in tutorial hell, thinking that just gluing pieces of code together was enough. It made sense for a while, but eventually, I wanted to quit. After some time, I searched online about how to improve, and people recommended learning data structures and algorithms (DSA). I didn’t know how to implement data structures on my own and struggled a lot—almost like going through hell. Nothing made sense. I couldn’t even get beyond linked lists. Then I found a Reddit post saying I might have the wrong mental model. It said that even if you know some programming, without understanding how to think about structures and systems, you're stuck. I started searching for what 'mental models' meant, but due to my lack of experience, I didn’t find anything useful. Later, I read a discussion on Hacker News about the book Structure and Interpretation of Computer Programs (SICP). One commenter explained how the book starts with the concept of data and procedures. That really clicked for me. I had never thought of computation as just transforming input data into output data. The model of: Input → Computation → Output made everything fall into place. This idea carries through all levels of programming—whether it's assembly, mid-level, or high-level languages. It’s the fundamental notion. You need basic constructs to steer data through transformations into useful outputs. Computation is essentially about transforming one form of information into another. After that realization, everything started to make sense—assembly, high-level programming, even operating systems. It was one of the best ‘aha!’ moments of my life."
- pillars 1y agoData, data, data :))) Some basic notions to know: Input → Computation → Output Information is omnipresent (this is just an intuition, not a claim). It serves as both input and output. Computation—also known as a procedure, function, set of instructions, transformation, method, algorithm, or calculation. In my early days, I ignored the fundamental notion of data and procedures. But eventually, it clicked: Programs = Data + Instructions Watch Feynman on computing—he even starts with the same concept of data, introducing computers as information processing systems. And processing requires algorithms (i.e., instructions or procedures). Programming is simply writing instructions for the computer to perform computations. A computer is just a machine for computing. Computation is a general idea: a transformation of one form of information into another.
- bitwize 1y agoMachine code ceased to be scary, or at least mysterious, to me when I opened the rudimentary debugger on a TRS-80. It was really more of a monitor, and it showed the contents of a certain chunk of memory in the top half of the screen. I loaded a program I was working on into it, and began changing instructions in memory, using the assembler output as my guide, and jumping into the program to see what the effects were. After that the little lightbulb went off. Oh, these are just bytes in memory that correspond to CPU instructions, and the CPU just reads them off and executes the instructions.
- xelxebar 1y agoOh, cool. A couple years ago I spent a few days disassembling a small x86-64 binary by hand. Getting familiar with the encoding was a lot of fun! The following reference was indispensable: http://ref.x86asm.net/ http://ref.x86asm.net/
- Leo-thorne 1y agoI always thought machine code was something only experts could understand. But after reading this article, I realized the basic concepts aren’t that complicated, it’s really just instructions, registers, and memory. I feel like this has given me a clearer understanding when it comes to writing code.
- flohofwoe 1y agoWrong article ;) (I think you mean https://news.ycombinator.com/item?id=44184900 https://news.ycombinator.com/item?id=44184900)
- Leo-thorne 1y agoThank you very much for your reminder. The comment has been corrected.
- 1Sebastian 1y ago[dead]
- Koshkin 1y agoIndeed. In Knuth’s Art the machine code was not the “scariest” part. (Programming is hard.)
- renox 1y agoThe thing is: most programmers see assembly language generated by a compiler, so no comment, and in optimised code with vector operations, it IS scary.
- egberts1 1y agoIt is downright scary, these assemblers. Tracking all those transitions of multiple micro-register states between each pair of opcode instructions, oy! Disclaimer: designed 6502 instruction set. Flabbergasted at today's Intel instruction set! Thank the gods for compilers!
- purplesyringa 1y agoIt's really interesting to see what people focus on when learning new things. Instruction encoding is something I would consider arcane knowledge, except that it isn't really useful; if you need to write an assembler or a disassembler, you can always look it up, but otherwise assembler instructions is basically all you need. Focusing on the nuances of a specific architecture's encoding is like people studying arithmetic instead of algebra and saying they now understand "math"; like, yes, a little, but there's so much you don't even know you don't know. I'm absolutely not criticizing the author here, machine code can absolutely be beautiful and I love the "it isn't scary" sentiment, it just surprised me how unique the focus is. In fact, I think this is the first time I've ever read a post on "low-level computer stuff isn't that hard" and it was about machine code rather than assembly. Then again, I can absolutely see the appeal. Architectures are fun, and I remember myself designing ones all the time in childhood during summer vacation. (Yes, I know I'm autistic.) Seeing tradeoffs between variable-length and fixed-length, short and long instructions, how register count affects code simplicity and the available opcode space, etc. kind of gives you an intuitive understanding of why real-world architectures look the way they do.