Homme, tu ne sais même pas la boîte de vers que tu viens d'ouvrir.
Ok, dans l'ancien temps, ils ne "lisaient pas vraiment le code" tant que les programmeurs donnaient aux ordinateurs une série d'instructions qu'ils pouvaient comprendre. Vous voyez, voici ce qu'est fondamentalement un ordinateur : vous avez une unité centrale qui effectue des calculs, et vous avez une certaine MÉMOIRE où les calculs sont stockés. Pour fabriquer l'unité centrale, les ingénieurs en matériel le construisent de manière à ce que certaines entrées donnent certaines sorties (par exemple, additionner deux nombres, déplacer un objet d'un endroit à un autre dans la mémoire, etc.) Ceci est intégré dans le matériel. C'est la ligne de base.
Ok, donc les anciens programmeurs étaient comme "Mec, c'est beaucoup de travail. Pour ajouter deux nombres, nous devons dire à notre ordinateur de
- déplacer la mémoire 0x1234 vers un registre
- puis déplacer la mémoire 0x1235 vers un autre registre
- puis additionner ces deux-là et stocker le résultat dans un troisième registre
- remettre le troisième registre en mémoire à 0x1236."
Ceci, en langage moderne, c'est a = b + c. Mon, comme les choses ont changé ! 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. Ici, le premier chiffre est l'instruction ou l'opération à effectuer (le matériel le reconnaît, aucune traduction n'est nécessaire), et les deuxième, troisième et (parfois) quatrième chiffres sont les opérandes de l'instruction.
Alors, qu'ont-ils fait pour rendre cela plus facile ? Eh bien, il s'avère que nous disons toujours à l'ordinateur de faire exactement cela, mais à un niveau beaucoup plus élevé. 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. Puis les compilateurs ont commencé à être construits. Prenons le compilateur C, certainement pas le premier, mais définitivement un succès. Le compilateur C prend un programme source en entrée, puis lui fait subir une série de manipulations.
Il lexe le programme source, en lisant des lettres et en sortant des parties de base du discours appelées tokens ou lexèmes. For example, the sample program
- int x;
- int main() { x = 1; }
pourrait être lexicalisé en tokens 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. Qui plus est, le compilateur doit ensuite attribuer une signification sémantique à ces choses. C'est une chose de dire "Hé, j'ai une instruction d'assignation", et une autre d'amener le compilateur à produire du code qui fait que l'ordinateur fait cela.
Après cela, le compilateur fait certaines choses à l'AST qu'il a créé, le rendant plus facile à raisonner. Il va essayer d'optimiser certaines choses, mais dans notre cas, supposons qu'il passe directement à la génération du code d'assemblage. Il effectuera une certaine configuration, en déterminant l'espace dont il a besoin pour exécuter chaque partie du programme (comme les fonctions, les données globales, etc.). 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. C'est un sujet cool si vous êtes intéressé.
EDIT : Consultez la réponse de Ryan Lam (il renvoie à un de ses précédents billets) si vous voulez approfondir le matériel.
La réponse de Ryan Lam est très intéressante.