Cómo pueden los ordenadores leer código?

Hombre, ni siquiera sabes la lata de gusanos que acabas de abrir.

Bien, en los viejos tiempos, realmente no «leían código» tanto como los programadores daban a los ordenadores una serie de instrucciones que podían entender. Verás, esto es lo que es básicamente un ordenador: tienes una CPU que hace cálculos, y tienes una MEMORIA donde se almacenan los cálculos. Para hacer la CPU, los ingenieros de hardware la construyen de manera que ciertas entradas den ciertas salidas (es decir, sumar dos números, mover una cosa de un lugar a otro en la memoria, etc). Esto se construye en el hardware. Esta es la línea base.

De acuerdo, así que los antiguos programadores eran como «Amigo, esto es un montón de trabajo. Para sumar dos números tenemos que decirle a nuestro ordenador que

  1. moviera la memoria 0x1234 a un registro
  2. luego mueva la memoria 0x1235 a otro registro
  3. luego sume esos dos y guarde el resultado en un tercer registro
  4. devuelva a poner el tercer registro en la memoria 0x1236.»

Esto, en lenguaje moderno, es a = b + c. ¡Cómo han cambiado las cosas! Now, keep in mind that instead of writing this into the computer, they would enter in numbers, such as

  1. 39 1 0x1234 ; 39 trg src: move memory src to register target 
  2. 39 2 0x1235 ; see above 
  3. 47 3 2 1 ; 47 trg src1 src2: store src1 + src2 in register target  
  4. 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. Aquí el primer número es la instrucción o la operación a realizar (el hardware lo reconoce, no es necesario traducirlo), y el segundo, tercer y (a veces) cuarto número son los operandos de la instrucción.

¿Y qué hicieron para facilitar esto? Pues resulta que seguimos diciéndole al ordenador que haga precisamente eso, sólo que a un nivel mucho más alto. First they built an assembly language, with human readable format (kinda) instead of numbers

  1. mov r1 0x1234 ; Move mem value at 0x1234 to r1 (first register) 
  2. mov r2 0x1235 ; Move mem value at 0x1235 to r2 (second register) 
  3. add r3 r1 r2 ; Add the contents of r1 and r2, storing in r3 
  4. 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. Luego se empezaron a construir compiladores. Tomemos el compilador de C, ciertamente no el primero, pero definitivamente uno exitoso. El compilador de C toma un programa fuente como entrada, y luego le hace una serie de manipulaciones.

Lexifica el programa fuente, leyendo en letras y emitiendo partes básicas del discurso llamadas tokens o lexemas. For example, the sample program

  1. int x; 
  2. int main() { x = 1; } 

Puede ser lexado 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

  1. PROGRAM 
  2. |– DECLVAR (type: int, name: «x») /* Declare a new var of type int named x */ 
  3. |– DECLFUNC (type:int, name:main) /* Declare a function of type int named main*/ 
  4. |– STATEMENTS /* A function has a series of statements */ 
  5. |– AssignStatement (=) /* An assign statement */ 
  6. |- LHS: x /* … assigning ‘1’ to x */ 
  7. |- RHS: 1  
  8.  
  9. /* 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. Además, el compilador tiene que asignar un significado semántico a estas cosas. Una cosa es decir «Hey, tengo una declaración de asignación», y otra conseguir que el compilador produzca código que haga que el ordenador haga eso.

Después de eso, el compilador hace algunas cosas al AST que ha creado, haciéndolo más fácil de razonar. Tratará de optimizar algunas cosas, pero en nuestro caso, vamos a suponer que va directamente a la generación de código ensamblador. Hará algunas configuraciones, calculando cuánto espacio necesita para ejecutar cada parte del programa (como las funciones, los datos 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):

  1. ; Assembly code for the C program above. Most of this is setting up the  
  2. ; environment in which the program will run. 
  3. .file «test.c» 
  4. .comm x,4,4 ; our x value 
  5. .text 
  6. .globl main 
  7. .type main, @function 
  8. main: 
  9. .LFB0: 
  10. .cfi_startproc 
  11. pushq %rbp ; Setup for the function (push base pointer) 
  12. .cfi_def_cfa_offset 16 ; Offset needed for function 
  13. .cfi_offset 6, -16 
  14. movq %rsp, %rbp ; Continue setup for function 
  15. .cfi_def_cfa_register 6 
  16. movl $1, x(%rip) ; Assigning 1 to x (This is our whole program) 
  17. movl $0, %eax ; Set up return val (defaults to zero) 
  18. popq %rbp ; Pop the base pointer 
  19. .cfi_def_cfa 7, 8 
  20. ret ; Return from function 
  21. .cfi_endproc 
  22. .LFE0: 
  23. ; Some information about the program, such as compiler version (GCC 5.4.0)  
  24. .size main, .-main 
  25. .ident «GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.4) 5.4.0 20160609» 
  26. .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. Es un tema interesante si te interesa.

EDIT: Mira la respuesta de Ryan Lam (enlaza a un post suyo anterior) si quieres profundizar en el hardware.