Man, non sai nemmeno il barattolo di vermi che hai appena aperto.
Okay, ai vecchi tempi, non hanno davvero "letto il codice" tanto quanto i programmatori hanno dato ai computer una serie di istruzioni che potevano capire. Vedete, questo è ciò che è fondamentalmente un computer: avete una CPU che fa i calcoli, e avete una MEMORIA dove vengono memorizzati i calcoli. Per fare la CPU, gli ingegneri hardware la costruiscono in modo che certi input diano certi output (cioè sommare due numeri, spostare una cosa da un posto all'altro della memoria, ecc.) Questo è costruito nell'hardware. Questa è la linea di base.
Ok, così gli antichi programmatori erano tipo "Amico, questo è un sacco di lavoro. Per sommare due numeri dobbiamo dire al nostro computer di
- spostare la memoria 0x1234 in un registro
- poi spostare la memoria 0x1235 in un altro registro
- poi sommare questi due e memorizzare il risultato in un terzo registro
- rimettere il terzo registro in memoria a 0x1236."
Questo, nel linguaggio moderno, è a = b + c. Accidenti, come sono cambiate le cose! Now, keep in mind that instead of writing this into the computer, they would enter in numbers, such as
- 39 1 0x1234 ; 39 trg src: move memory src to register target
- 39 2 0x1235 ; see above
- 47 3 2 1 ; 47 trg src1 src2: store src1 + src2 in register target
- 38 0x1236 3 ; 38 trg src: move register src to memory location trg
Now obviously I entered in the numbers in decimal/hex and have nice comments on the sides (starting with ‘;’ til the end of the line) and they would have to find some way of manually punching these in, either through punch cards or some other electronic means. Qui il primo numero è l'istruzione o l'operazione da eseguire (l'hardware lo riconosce, nessuna traduzione necessaria), e il secondo, terzo e (a volte) quarto numero sono gli operandi dell'istruzione. Beh, si scopre che stiamo ancora dicendo al computer di fare proprio questo, solo ad un livello molto più alto. First they built an assembly language, with human readable format (kinda) instead of numbers
- mov r1 0x1234 ; Move mem value at 0x1234 to r1 (first register)
- mov r2 0x1235 ; Move mem value at 0x1235 to r2 (second register)
- add r3 r1 r2 ; Add the contents of r1 and r2, storing in r3
- mov 0x1236 r3 ; Move the register r3 to memory 0x1236
This is pretty similar to the above but it made it easier for programmers to work. Poi i compilatori hanno cominciato ad essere costruiti. Prendiamo il compilatore C, certamente non il primo, ma sicuramente uno di successo. Il compilatore C prende un programma sorgente come input, e poi fa una serie di manipolazioni su di esso.
Legge il programma sorgente, leggendo le lettere e producendo parti fondamentali del discorso chiamate token o lessemi. For example, the sample program
- int x;
- int main() { x = 1; }
potrebbe essere lessato nei token IDENT("int"), IDENT("x"), IDENT("int"), IDENT("main"), LPAREN, RPAREN, LBRACE, IDENT("x"), EQ, INT(1), SEMI, RBRACE. From here, the parser will turn this into an AST or Abstract Syntax Tree, a rough version looking like this
- PROGRAM
- |-- DECLVAR (type: int, name: "x") /* Declare a new var of type int named x */
- |-- DECLFUNC (type:int, name:main) /* Declare a function of type int named main*/
- |-- STATEMENTS /* A function has a series of statements */
- |-- AssignStatement (=) /* An assign statement */
- |- LHS: x /* ... assigning '1' to x */
- |- RHS: 1
- /* We have a program the declares a function that has a list of statements that has a single statement that assigns 1 to x */
This is pretty simple, but it can get wildly more complicated. Inoltre, il compilatore deve assegnare un significato semantico a queste cose. Una cosa è dire "Ehi, ho una dichiarazione di assegnazione", e un'altra è fare in modo che il compilatore produca del codice che lo faccia fare al computer.
Dopo questo, il compilatore fa alcune cose all'AST che ha creato, rendendolo più facile per lui da ragionare. Cercherà di ottimizzare alcune cose, ma nel nostro caso, assumiamo che vada direttamente alla generazione di codice assembly. Farà alcune impostazioni, calcolando quanto spazio ha bisogno per eseguire ogni parte del programma (come funzioni, dati globali, ecc.). For example, it might need 16 bytes to deal with the main function’s locations, and another four bytes to store the variable x (outside of the function in global space). It will then put this down into code, and write it to a file:
Here’s what GCC produces from that code I wrote above (in assembly):
- ; Assembly code for the C program above. Most of this is setting up the
- ; environment in which the program will run.
- .file "test.c"
- .comm x,4,4 ; our x value
- .text
- .globl main
- .type main, @function
- main:
- .LFB0:
- .cfi_startproc
- pushq %rbp ; Setup for the function (push base pointer)
- .cfi_def_cfa_offset 16 ; Offset needed for function
- .cfi_offset 6, -16
- movq %rsp, %rbp ; Continue setup for function
- .cfi_def_cfa_register 6
- movl $1, x(%rip) ; Assigning 1 to x (This is our whole program)
- movl $0, %eax ; Set up return val (defaults to zero)
- popq %rbp ; Pop the base pointer
- .cfi_def_cfa 7, 8
- ret ; Return from function
- .cfi_endproc
- .LFE0:
- ; Some information about the program, such as compiler version (GCC 5.4.0)
- .size main, .-main
- .ident "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.4) 5.4.0 20160609"
- .section .note.GNU-stack,"",@progbits
Once in assembly code, different files can be linked together and turned into machine code, which is just a bunch of bytes that make sense to the computer.
Anyways, this is a very abbreviated intro to compilers. È un argomento interessante se siete interessati.
EDIT: Guardate la risposta di Ryan Lam (si collega ad un suo post precedente) se volete andare più in profondità nell'hardware.