Assembly language is converted in to machine language by assembler. Why would a compiler convert high-level language to assembly? Can't it directly convert from the high-level language to machine code?
Other reason for compilers to produce assembly rather than proper machine code are:
- The symbolic addresses used by assemblers instead of hard-coding machine addresses make code relocation much easier.
- Linking code may involve safety checks such as type-checking, and that's easier to do with symbolic names.
- Small changes in machine code are easier to accomodate by changing the assembler rather than the code generator.
A compiler does usually convert high-level code directly to machine language, but it can be built in a modular way so that one back-end emits machine code and the other assembly code (like GCC). The code generation phase produces "code" which is some internal representation of machine code, which then has to be converted to a usable format like machine language or assembly code.
Historically a number of notable compilers did output machine code directly. There are some difficulties with doing so, however. Generally someone who's trying to confirm that a compiler is working correctly will find it easier to examine assembly-code output than machine code. Further, it's possible (and was historically common) to use a one-pass C or Pascal compiler to produce an assembly-language file which can then be processed using a two-pass assembler. Generating code directly would require either using a two-pass C or Pascal compiler or else using a single-pass compiler followed by some means of back-patching forward-jump addresses [if a runtime environment makes the size of a launched program available in a fixed spot, a compiler could write a list of patches at the end of the code and have startup code apply those patches at runtime; such an approach would increase the executable size by about four bytes per patch-point, but would improve program-generation speed].
If the goal is to have a compiler that runs quickly, direct code generation can work well. For most projects, however, the cost of generating the assembly-language code and assembling it really isn't a major problem nowadays. Having compilers produce code in a form that can interact nicely with code produced by other compilers is generally a big enough benefit to justify the increase in compilation times.
Even platforms that use the same instruction set may have different relocatable object file formats. I can think of "a.out" (early UNIX), OMF, MZ (MS-DOS EXE), NE (16-bit Windows), COFF (UNIX System V), Mach-O (OS X and iOS), and ELF (Linux and others), as well as variants of those, such as XCOFF (AIX), ECOFF (SGI), and COFF-based Portable Executable (PE) on 32-bit Windows. A compiler that produces assembly language need not know much about object file formats, allowing the assembler and linker to encapsulate that knowledge in a separate process.
See also Difference between OMF and COFF on Stack Overflow.
Usually compilers work internally with sequences of instructions. Each instruction will be represented by a data structure representing it's operation name, operands and so-on. When the operands are addresses those addresses will usually be symbolic references, not concrete values.
Outputting assembler is relatively simple. It's pretty much a matter of taking the compilers internal data structure and dumping it to a text file in a specific format. Assembler output is also relatively easy to read which is useful when you need to check what the compiler is doing.
Outputting binary object files is significantly more work. The compiler writer needs to know how all the instructions are encoded (which can be far from trivial on some CPUS), they need to convert some symbolic references to program counter relative addresses and others in to some form of meta data in the binary object file. They need to write everything out in a format that is highly system specific.
Yes you absolutely can make a compiler that can outputs binary objects directly without writing out assembler as an intermediate step. The question like so many things in software development is whether the reduction in compilation time is worth the extra development and maintenance work.
The compiler i'm most familiar with (freepascal) can output assembler on all platforms but can only output binary objects directly on a subset of platforms.
A compiler should be able to produce an assembler output in addition to the normal relocatable code is for the benefit of the programmer.
One time I just not find the bug in a C program running on Unix System V on an LSI-11 machine. Nothing seemed to work. Finally in desperation I had the protable C compiler excrete an assembler version of its translation. I had finally found the bug! The compiler was allocating more registers than existed in the machine! (The compiler allocated registers R0 through R8 on a machine with only registers R0 through R7.) I managed to work around the bug in the compiler and my program worked.
Another benefit of having assembler output is trying to use "standard" libraries that use a different parameter passing protocols. Later C compilers allow me to set the protocol with a parameter ("pascal" would make the compiler add the parameters in the order given as opposed to the C standard of reversing the order).
Yet another benefit is allowing the programmer to see what an appalling job his compiler is doing. A simple C statement takes about 44 machine instruction. Values are loaded from memory and then quickly discarded. etc, etc, etc ...
I personally believe that having a compiler instead of a relocatable object module is really stupid. While compiling your program, the compiler gathers a great deal of information about your program. It usually stores all this information in something called a Symbol Table. After excreting the assembler code it throws all this information table. The assembler then examines the excreted code and re-collects some of the information the the compiler already had. However assembler know nothing about If statements of For statements or While statements. So all this information is missing. Then the assembler produces the relocatable object module that the compiler did not.