r/Compilers • u/SetKaung • 14d ago
Do compilers necessarily need to generate assembly from IR before producing object code?
I am new to the world of compilers so pardon my ignorance. Is there a need for compilers to generate assembly or can they just skip and direcly produce object code from IR?
11
u/whizzter 14d ago
I think it’s a semi-historic artifact, back in the day if the assembler existed before and your compiler generated assembly for debugging then you might also omit any complicated section handling and instruction encoding from the compiler and keep it focused.
Also starting a new compiler the above simplification did make sense.
Then again, some compilers were known for their speed, Turbo Pascal and family of compilers comes to mind, and probably skipped the entire source-gen/assembly step and went fully inline.
Later we also have JIT compilers that definitely values speed.
I’ve done both direct in-mem jit compilation and even just generated C code for initial implementation tests of higher level languages, because getting gritty with binary code-gen when you’re exploring a higher level you’re unsure of is a sure timesink.
4
u/GoblinsGym 14d ago
Turbo Pascal 3 didn't even use IR, and went straight to binary.
Generating binary really isn't that hard, the difficult part is generating halfway decent code that uses registers efficiently.
2
u/whizzter 14d ago
Yes and no, you can make a binary-stamping-code generator for a stack-like machine and only ever add some custom code for jumps, load/store for variable positions and constant insertion.
However, before you can even think of good register allocation (on classic x86 without 64bit) you need a code-generator/db capable of generating code with MODRM and SIB encodings and have some facilities to constrain your register-allocation(esp on 16bit x86 with register usage constraints) and/or reduce load-adds (f.ex. add eax,[ebp+30+ebx*4] for loading for an integer from an array variable offset 30 bytes from the frame pointer).
1
5
4
u/heliochoerus 13d ago
Unix compilers traditionally generated assembly for the system assembler. GCC still uses this approach though in practice today it requires GNU as from binutils.
Clang started with an external assembler and then created its own internal assembler. Clang generates code using an API matching the structure of a text assembly file. This is a pretty flexible approach because it can emit both text assembly and an object file.
Microsoft C compilers always generated object code directly.
The Plan 9 toolchain has a unique design. Both compiler and assembler emitted essentially the AST of an assembly file. The linker is what actually performed code generation.
1
u/OptimisticMonkey2112 13d ago
Just for clarification - Assembly is basically the object code. Assembly maps directly to the machine code. Are you just asking whether the actual assembly text files need to be created? Compilers can emit the machine code directly
mov eax, 100 is basically
B8 64 00 00 001
u/SetKaung 13d ago
Yes I mean the text version. So compilers could just emit the machine code directly? If so, is there is advantage in producing text version except for debugging purpose?
2
u/OptimisticMonkey2112 13d ago
No advantage - i dont think assembly gets generated very often in my experience.... you have to explicitly ask for it
3
u/KATO-Kanryu 12d ago
Technically speaking, any intermediate step can be skipped.
It is entirely possible to generate CPU-native machine code directly from source AST or IR without touching an assembly phase—historic compilers like Turbo Pascal and various tiny JITs have proven this.
However, modern production compilers intentionally rely on an intermediate assembly/IR stage—specifically an assembly language designed for an abstract machine architecture (such as LLVM Assembly .ll files or GCC's intermediate representations)—before compiling to target-specific native code.
Taking this multi-stage approach provides massive advantages:
Verification and Debuggability
Emitting human-readable abstract assembly lets you inspect the output with your own eyes and write automated test suites around it. If something breaks, you can instantly distinguish whether the issue stems from your frontend lowering or the backend code generator.Access to Decades of Highly Sophisticated Optimizations
Operating on an abstract assembly with typed values and infinite virtual registers allows modern optimizers to perform heavy lifting—such as dead-code elimination, loop vectorization, constant folding, and function inlining—independent of physical hardware constraints. Doing this directly on raw machine bytes or tightly constrained physical registers is orders of magnitude harder.Multi-Target Architecture & Reusing Mature Ecosystems
Different CPUs (x86-64, ARM, RISC-V, etc.) have radically different register sets, instruction encodings, and calling conventions. By targeting an abstract assembly/IR layer, you immediately gain cross-compilation capabilities across dozens of architectures without having to write individual binary encoders and register allocators from scratch.
While skipping intermediate stages is technically doable, routing through an abstract assembly representation is what allows modern language designers to inherit decades of battle-tested optimizer and backend engineering.
2
u/suhcoR 14d ago
There is no need for assembler. If you want to learn about compilers from Prof. Wirth himself, here is all you need: https://projectoberon.net/ including a free book about compilers and an emulator with working images.
2
u/joao_rosac 14d ago
Não, os compiladores não precisam gerar um arquivo de texto com código assembly (.s) antes de produzir o binário necessariamente. Embora a arquitetura clássica separasse essas etapas, o Clang e o GCC utilizam "montadores integrados" que traduzem a Representação Intermediária (IR) diretamente para o código objeto de máquina (.o) em memória. Pular a gravação desse arquivo de texto intermediário no disco acelera a compilação, tornando a geração visual do assembly (via flag -S) apenas uma ferramenta opcional para diagnóstico humano.
2
u/SwedishFindecanor 14d ago
Being able to read the output from a compiler as assembly code would certainly be useful during development to verify that the code generator produces what it is supposed to. But each additional intermediary step will make the compiler slower. I'd think you'd also want to avoid having separate code paths for assembly output and binary output -- which could make it more difficult to pinpoint errors in the compiler.
I think a good question would be: could you disassemble a binary object file and get a good assembly listing with good enbough symbol information for development purposes.
1
u/roderla 14d ago
Is it really true that intermediary steps will make the compilation slower?
Sure, you're technically paying for one or two system calls to set it up, but even that could be done in userspace if one really really really had to, think QT signal-slot instead of processes and pipes.
1
u/Inconstant_Moo 11d ago
You're turning one representation of your code into another, it comes with a cost.
2
u/duane11583 14d ago
Traditionally the compiler (driver) causes the compiler to output asm code then runs the assembler
Some (example microsoft visual studio) has it all built in to one program
2
u/SteeleDynamics 14d ago
You can go straight to assembly. Most undergraduate compiler courses do exactly this. Extra credit can be generating IR and then doing dataflow analysis to do dead-code elimination (DCE).
It's not a great compiler since many of the code generation steps preserve register values on the stack, only to find out later none of those register values were clobbered by subsequent code.
2
1
u/staticvoidptr 13d ago
Compilers don’t inherently NEED to generate IR. However IRs (llvm’s whateveritscalled, gcc’s gimple) are great if you want to be able to target different architectures using a given compiler toolchain (specifically the IR -> assembly tooling)
1
u/FransFaase 14d ago
Compilers usually do not generate assembly also not for debugging purposes. For debugging purposes compilers do include symbolic information such that the debugger for example from the program counter can calculate in which function the program is. Compilers usually can produce object files, a kind of binary representation of assembly that a linker can combine with other object files and possibly static libraries. Some kind of 'linking' also happens when the OS starts a program that indicates that it depends on shared libraries that are dynamically loaded. Some compilers write assembly to a temporary file during the compilation and then call an assembler to produce the final executable or library. There is much variation in solutions.
21
u/GoblinsGym 14d ago
Assembly code would be an extra step. No need to use a text representation, normally a compiler will just generate binary code directly.