UP | HOME

Código fuente, objeto y ejecutable

Table of Contents

Código fuente, objeto y ejecutable

Hoy en día no se escribe código máquina directamente, así que se necesita de algún mecanismo o forma de obtener dicho código máquina que pueda ejecutar el ordenador.

Antes tienes que saber que existen tres tipos de código: código fuente, código objeto y código máquina o ejecutable.

Código fuente

Se trata de los ficheros con el código escrito en alto nivel del lenguaje de programación escogido para la programación. También se denomina código fuente a los programas escritos en ensamblador.

Un programa puede estar escrito en uno o varios ficheros.

A continuación te muestro el código fuente de un programa escrito en C y su equivalente en ensamblador. Se trata de un programa que suma dos números y muestra el resultado por pantalla.

#include <stdio.h>

int main(int argc, char **argv)
{
    int num1, num2, resultado;

    num1 = 15;
    num2 = 20;

    resultado = num1 + num2;

    printf("Resultado de sumar %d y %d = %d\n", num1, num2, resultado);

    return 0;
}
    .file   "main.c"
    .text
    .section    .rodata
    .align 8
.LC0:
    .string "Resultado de sumar %d y %d = %d\n"
    .text
    .globl  main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    subq    $32, %rsp
    movl    %edi, -20(%rbp)
    movq    %rsi, -32(%rbp)
    movl    $15, -4(%rbp)
    movl    $20, -8(%rbp)
    movl    -4(%rbp), %edx
    movl    -8(%rbp), %eax
    addl    %edx, %eax
    movl    %eax, -12(%rbp)
    movl    -12(%rbp), %ecx
    movl    -8(%rbp), %edx
    movl    -4(%rbp), %eax
    movl    %eax, %esi
    leaq    .LC0(%rip), %rdi
    movl    $0, %eax
    call    printf@PLT
    movl    $0, %eax
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .ident  "GCC: (Debian 10.2.1-6) 10.2.1 20210110"
    .section        .note.GNU-stack,"",@progbits

Código objeto

Es el código binario resultante de traducir cada fichero fuente. Este código no es inteligible por el ser humano pero tampoco está listo para ser ejecutado por la CPU.

Aquí te muestro el código desensamblado en hexadecimal, donde se indican las direcciones de la memoria virtual donde se cargará el programa y las instrucciones, también en hexadecimal.

Por cuestiones de claridad, a la derecha tienes el código ensamblador equivalente.

0000000000000000 <main>:
0:  55                      push   %rbp
1:  48 89 e5                mov    %rsp,%rbp
4:  48 83 ec 20             sub    $0x20,%rsp
8:  89 7d ec                mov    %edi,-0x14(%rbp)
b:  48 89 75 e0             mov    %rsi,-0x20(%rbp)
f:  c7 45 fc 0f 00 00 00    movl   $0xf,-0x4(%rbp)
16: c7 45 f8 14 00 00 00    movl   $0x14,-0x8(%rbp)
1d: 8b 55 fc                mov    -0x4(%rbp),%edx
20: 8b 45 f8                mov    -0x8(%rbp),%eax
23: 01 d0                   add    %edx,%eax
25: 89 45 f4                mov    %eax,-0xc(%rbp)
28: 8b 4d f4                mov    -0xc(%rbp),%ecx
2b: 8b 55 f8                mov    -0x8(%rbp),%edx
2e: 8b 45 fc                mov    -0x4(%rbp),%eax
31: 89 c6                   mov    %eax,%esi
33: 48 8d 3d 00 00 00 00    lea    0x0(%rip),%rdi        # 3a <main+0x3a>
3a: b8 00 00 00 00          mov    $0x0,%eax
3f: e8 00 00 00 00          callq  44 <main+0x44>
44: b8 00 00 00 00          mov    $0x0,%eax
49: c9                      leaveq
4a: c3                      retq

Este código, aunque está ya en binario, no está listo para ser cargado en memoria y empezar su ejecución, falta un paso más, su adaptación a código máquina.

Código máquina

También llamado código ejecutable, es el código binario resultante de enlazar los archivos con el código objeto y ciertas rutinas y bibliotecas necesarias.

Siguiendo con el ejemplo anterior, aquí te muestro el código ejecutable desensamblado. Como ves es bastante más largo que el código objeto anterior porque se han añadido ciertas rutinas y bibliotecas necesarias, todo en un mismo fichero.

Por cuestiones de claridad, aquí también te muestro a la derecha el código ensamblador equivalente.

0000000000001000 <_init>:
    1000:   48 83 ec 08             sub    $0x8,%rsp
    1004:   48 8b 05 dd 2f 00 00    mov    0x2fdd(%rip),%rax        # 3fe8 <__gmon_start__>
    100b:   48 85 c0                test   %rax,%rax
    100e:   74 02                   je     1012 <_init+0x12>
    1010:   ff d0                   callq  *%rax
    1012:   48 83 c4 08             add    $0x8,%rsp
    1016:   c3                      retq

Desensamblado de la sección .plt:

0000000000001020 <.plt>:
    1020:   ff 35 e2 2f 00 00       pushq  0x2fe2(%rip)        # 4008 <_GLOBAL_OFFSET_TABLE_+0x8>
    1026:   ff 25 e4 2f 00 00       jmpq   *0x2fe4(%rip)        # 4010 <_GLOBAL_OFFSET_TABLE_+0x10>
    102c:   0f 1f 40 00             nopl   0x0(%rax)

0000000000001030 <printf@plt>:
    1030:   ff 25 e2 2f 00 00       jmpq   *0x2fe2(%rip)        # 4018 <printf@GLIBC_2.2.5>
    1036:   68 00 00 00 00          pushq  $0x0
    103b:   e9 e0 ff ff ff          jmpq   1020 <.plt>

Desensamblado de la sección .plt.got:

0000000000001040 <__cxa_finalize@plt>:
    1040:   ff 25 b2 2f 00 00       jmpq   *0x2fb2(%rip)        # 3ff8 <__cxa_finalize@GLIBC_2.2.5>
    1046:   66 90                   xchg   %ax,%ax

Desensamblado de la sección .text:

0000000000001050 <_start>:
    1050:   31 ed                   xor    %ebp,%ebp
    1052:   49 89 d1                mov    %rdx,%r9
    1055:   5e                      pop    %rsi
    1056:   48 89 e2                mov    %rsp,%rdx
    1059:   48 83 e4 f0             and    $0xfffffffffffffff0,%rsp
    105d:   50                      push   %rax
    105e:   54                      push   %rsp
    105f:   4c 8d 05 7a 01 00 00    lea    0x17a(%rip),%r8        # 11e0 <__libc_csu_fini>
    1066:   48 8d 0d 13 01 00 00    lea    0x113(%rip),%rcx        # 1180 <__libc_csu_init>
    106d:   48 8d 3d c1 00 00 00    lea    0xc1(%rip),%rdi        # 1135 <main>
    1074:   ff 15 66 2f 00 00       callq  *0x2f66(%rip)        # 3fe0 <__libc_start_main@GLIBC_2.2.5>
    107a:   f4                      hlt
    107b:   0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)

0000000000001080 <deregister_tm_clones>:
    1080:   48 8d 3d a9 2f 00 00    lea    0x2fa9(%rip),%rdi        # 4030 <__TMC_END__>
    1087:   48 8d 05 a2 2f 00 00    lea    0x2fa2(%rip),%rax        # 4030 <__TMC_END__>
    108e:   48 39 f8                cmp    %rdi,%rax
    1091:   74 15                   je     10a8 <deregister_tm_clones+0x28>
    1093:   48 8b 05 3e 2f 00 00    mov    0x2f3e(%rip),%rax        # 3fd8 <_ITM_deregisterTMCloneTable>
    109a:   48 85 c0                test   %rax,%rax
    109d:   74 09                   je     10a8 <deregister_tm_clones+0x28>
    109f:   ff e0                   jmpq   *%rax
    10a1:   0f 1f 80 00 00 00 00    nopl   0x0(%rax)
    10a8:   c3                      retq
    10a9:   0f 1f 80 00 00 00 00    nopl   0x0(%rax)

00000000000010b0 <register_tm_clones>:
    10b0:   48 8d 3d 79 2f 00 00    lea    0x2f79(%rip),%rdi        # 4030 <__TMC_END__>
    10b7:   48 8d 35 72 2f 00 00    lea    0x2f72(%rip),%rsi        # 4030 <__TMC_END__>
    10be:   48 29 fe                sub    %rdi,%rsi
    10c1:   48 89 f0                mov    %rsi,%rax
    10c4:   48 c1 ee 3f             shr    $0x3f,%rsi
    10c8:   48 c1 f8 03             sar    $0x3,%rax
    10cc:   48 01 c6                add    %rax,%rsi
    10cf:   48 d1 fe                sar    %rsi
    10d2:   74 14                   je     10e8 <register_tm_clones+0x38>
    10d4:   48 8b 05 15 2f 00 00    mov    0x2f15(%rip),%rax        # 3ff0 <_ITM_registerTMCloneTable>
    10db:   48 85 c0                test   %rax,%rax
    10de:   74 08                   je     10e8 <register_tm_clones+0x38>
    10e0:   ff e0                   jmpq   *%rax
    10e2:   66 0f 1f 44 00 00       nopw   0x0(%rax,%rax,1)
    10e8:   c3                      retq
    10e9:   0f 1f 80 00 00 00 00    nopl   0x0(%rax)

00000000000010f0 <__do_global_dtors_aux>:
    10f0:   80 3d 39 2f 00 00 00    cmpb   $0x0,0x2f39(%rip)        # 4030 <__TMC_END__>
    10f7:   75 2f                   jne    1128 <__do_global_dtors_aux+0x38>
    10f9:   55                      push   %rbp
    10fa:   48 83 3d f6 2e 00 00    cmpq   $0x0,0x2ef6(%rip)        # 3ff8 <__cxa_finalize@GLIBC_2.2.5>
    1101:   00
    1102:   48 89 e5                mov    %rsp,%rbp
    1105:   74 0c                   je     1113 <__do_global_dtors_aux+0x23>
    1107:   48 8b 3d 1a 2f 00 00    mov    0x2f1a(%rip),%rdi        # 4028 <__dso_handle>
    110e:   e8 2d ff ff ff          callq  1040 <__cxa_finalize@plt>
    1113:   e8 68 ff ff ff          callq  1080 <deregister_tm_clones>
    1118:   c6 05 11 2f 00 00 01    movb   $0x1,0x2f11(%rip)        # 4030 <__TMC_END__>
    111f:   5d                      pop    %rbp
    1120:   c3                      retq
    1121:   0f 1f 80 00 00 00 00    nopl   0x0(%rax)
    1128:   c3                      retq
    1129:   0f 1f 80 00 00 00 00    nopl   0x0(%rax)

0000000000001130 <frame_dummy>:
    1130:   e9 7b ff ff ff          jmpq   10b0 <register_tm_clones>

0000000000001135 <main>:
    1135:   55                      push   %rbp
    1136:   48 89 e5                mov    %rsp,%rbp
    1139:   48 83 ec 20             sub    $0x20,%rsp
    113d:   89 7d ec                mov    %edi,-0x14(%rbp)
    1140:   48 89 75 e0             mov    %rsi,-0x20(%rbp)
    1144:   c7 45 fc 0f 00 00 00    movl   $0xf,-0x4(%rbp)
    114b:   c7 45 f8 14 00 00 00    movl   $0x14,-0x8(%rbp)
    1152:   8b 55 fc                mov    -0x4(%rbp),%edx
    1155:   8b 45 f8                mov    -0x8(%rbp),%eax
    1158:   01 d0                   add    %edx,%eax
    115a:   89 45 f4                mov    %eax,-0xc(%rbp)
    115d:   8b 4d f4                mov    -0xc(%rbp),%ecx
    1160:   8b 55 f8                mov    -0x8(%rbp),%edx
    1163:   8b 45 fc                mov    -0x4(%rbp),%eax
    1166:   89 c6                   mov    %eax,%esi
    1168:   48 8d 3d 99 0e 00 00    lea    0xe99(%rip),%rdi        # 2008 <_IO_stdin_used+0x8>
    116f:   b8 00 00 00 00          mov    $0x0,%eax
    1174:   e8 b7 fe ff ff          callq  1030 <printf@plt>
    1179:   b8 00 00 00 00          mov    $0x0,%eax
    117e:   c9                      leaveq
    117f:   c3                      retq

0000000000001180 <__libc_csu_init>:
    1180:   41 57                   push   %r15
    1182:   4c 8d 3d 5f 2c 00 00    lea    0x2c5f(%rip),%r15        # 3de8 <__frame_dummy_init_array_entry>
    1189:   41 56                   push   %r14
    118b:   49 89 d6                mov    %rdx,%r14
    118e:   41 55                   push   %r13
    1190:   49 89 f5                mov    %rsi,%r13
    1193:   41 54                   push   %r12
    1195:   41 89 fc                mov    %edi,%r12d
    1198:   55                      push   %rbp
    1199:   48 8d 2d 50 2c 00 00    lea    0x2c50(%rip),%rbp        # 3df0 <__do_global_dtors_aux_fini_array_entry>
    11a0:   53                      push   %rbx
    11a1:   4c 29 fd                sub    %r15,%rbp
    11a4:   48 83 ec 08             sub    $0x8,%rsp
    11a8:   e8 53 fe ff ff          callq  1000 <_init>
    11ad:   48 c1 fd 03             sar    $0x3,%rbp
    11b1:   74 1b                   je     11ce <__libc_csu_init+0x4e>
    11b3:   31 db                   xor    %ebx,%ebx
    11b5:   0f 1f 00                nopl   (%rax)
    11b8:   4c 89 f2                mov    %r14,%rdx
    11bb:   4c 89 ee                mov    %r13,%rsi
    11be:   44 89 e7                mov    %r12d,%edi
    11c1:   41 ff 14 df             callq  *(%r15,%rbx,8)
    11c5:   48 83 c3 01             add    $0x1,%rbx
    11c9:   48 39 dd                cmp    %rbx,%rbp
    11cc:   75 ea                   jne    11b8 <__libc_csu_init+0x38>
    11ce:   48 83 c4 08             add    $0x8,%rsp
    11d2:   5b                      pop    %rbx
    11d3:   5d                      pop    %rbp
    11d4:   41 5c                   pop    %r12
    11d6:   41 5d                   pop    %r13
    11d8:   41 5e                   pop    %r14
    11da:   41 5f                   pop    %r15
    11dc:   c3                      retq
    11dd:   0f 1f 00                nopl   (%rax)

00000000000011e0 <__libc_csu_fini>:
    11e0:   c3                      retq

Desensamblado de la sección .fini:

00000000000011e4 <_fini>:
    11e4:   48 83 ec 08             sub    $0x8,%rsp
    11e8:   48 83 c4 08             add    $0x8,%rsp
    11ec:   c3                      retq

Carga del programa ejecutable

El código ejecutable tiene que tener un formato que entienda el sistema operativo sobre el que se vaya a ejecutar. A continuación, te explico brevemente, los formatos usados en los sistemas operativos Linux y Windows:

  • En Linux el formato se denomina ELF (Executable and Linkable Format). Estos ficheros ejecutables tiene una cabecera (ELF header), el .text donde están las instrucciones del programa y el .data donde están los datos del mismo (entre otras cosas). En la Wikipedia puedes ver los detalles de estos ejecutables.

elf_format.png

Figure 1: Formato de los ficheros ejecutables ELF

  • En Windows el formato se denomina EXE (de EXEcutable) y que, internamente, se conoce como PE (Portable Executable). Estos ficheros tienen, también, una cabecera (DOS header) y una serie de punteros (que se pueden ver debajo en la siguiente imagen) a las instrucciones y los datos del programa. En la propia web de Microsoft puedes leer los detalles de los Portable Executable para los programas de 32 bits.

exe_format.png

Figure 2: Formato de los ficheros ejecutable EXE

Por ese motivo, un programa escrito y convertido para ser ejecutado en Linux no puede ser ejecutado en Windows, y viceversa.

Ejecución de un programa en el ordenador

Vamos a ver en este apartado cómo se ejecuta un programa, ya traducido a código máquina, en la Arquitectura von Neumann, de manera que acabemos de entender la función de cada una de las partes de dicha arquitectura y cómo se relaciona y funcionan conjuntamente el software y el hardware.

Necesitas tener presente la arquitectura de von Neumann para entender todo lo que te voy a explicar en este apartado:

von_neumann.png

Figure 3: Arquitectura de von Neumann

Carga del programa en memoria

Antes, vamos a recordar cómo es una memoria, a nivel físico, en la Arquitectura von Neumann. En la imagen tienes representada una memoria con estas características:

  • Arquitectura de 8 bytes: registros de la CPU de 8 bits y filas de la memoria RAM de 8 bits (cada una de las celdas es un bit).
  • Las direcciones son de las filas de la memoria y están dadas en hexadecimal (los número comienzan con el prefijo 0x para indicar que dicho número está en formato hexadecimal).
  • Se trata, pues, de una memoria de 256 bytes, es decir, hay 2 048 celdas o bits.

memoria_256bytes.png

Figure 4: Representación de una memoria de 4GiB

Cuando ejecutas un programa (abres un programa) el sistema operativo se encarga de cargarlo en la memoria principal. Como, en dicha memoria, pudiera haber más programas porque, hoy en día, se pueden estar ejecutando varios programas al mismo tiempo en un mismo ordenador, el sistema operativo utiliza una serie de algoritmos para buscar un lugar donde cargar el nuevo programa a ejecutar.

Este podría ser el estado en que se encuentra la memoria con el nuevo programa cargado. Como ves en la imagen, cada programa cargado en memoria se divide en varias secciones que, por simplificar, he reducido en tres:

  • Los datos de solo lectura .rodata donde están los valores constantes que no cambian durante la ejecución del programa. Esta memoria es fija y conocida en tiempo de ejecucion.
  • La sección con los datos .data, espacio reservado para almacenar datos que varía a lo largo de la ejecución del programa. A estos datos se les denomina variables. Esta memoria varía a lo largo de la ejecución del programa. Habrá momentos que ocupe más y momentos en los que ocupe menos.
  • Y, por último, la sección donde se encuentran las instrucciones traducidas a lenguaje máquina. A esta sección se le conoce comúnmente como .text. El tamaño de esta sección es fija y conocida en tiempo de ejecución.

memoria_cargada.png

Figure 5: Memoria con varios programas cargados

Como vemos en la imagen teníamos 2 programas ejecutándose en memoria: uno en la dirección 0x3e8 y otro en la dirección 0xdac. El sistema operativo decidió cargar nuestro programa en la dirección 0x7d0. Además, fíjate que las primeras direcciones de memoria son las que se utilizan para el sistema operativo que es el programa que se carga durante el arranque del ordenador.

Cuando se cierra una programa, el sistema operativo liberará su espacio en memoria para que lo pueda ocupar otro programa en ejecución.

Aunque está fuera del alcance de esta unidad, los programas en ejecución, una vez cargados en la memoria, se conocen como procesos. Quédate con este nombre porque será importante en otros momentos.

Ejecución de un programa

Una vez cargado el programa en memoria se puede pasar a su ejecución.

La manera en que el sistema operativo gestiona todos los programas en ejecución queda fuera del alcance de este módulo, lo estudiarás en otros módulos. Así pues, vamos a suponer que solo tiene que ejecutar nuestro programa.

Los programas son secuencias de instrucciones que se ejecutan en orden, una detrás de otra. A veces se rompe esta secuencia con instrucciones de salto que hacen que el programa, de repente, vaya atrás o adelante, permitiendo bucles y llamadas a rutinas. Una vez acabado el salto, el bucle o la llamada a una rutina, se vuelve a la secuencia de instrucciones.

Esta ejecución se hace dentro de la CPU, para lo cual se tienen que ir cargando las instrucciones en el registro de instrucciones (RI) del microprocesador de una en una. Estas instrucciones viaja de la memoria la CPU. Qué instrucciones es la siguiente se sabe porque la CPU mantienen la dirección de memoria de la siguiente instrucción a ejecutar en el registro contador de programa (PC).

Vamos a imaginar un programa muy sencillo que consta de estas tres instrucciones en ensamblador:

movl    -8(%rbp), %edx
movl    -16(%rbp), %eax
addl    %edx, %eax

Aunque no es importante para conocer el funcionamiento de la ejecución de un programa en la CPU, sí te puedo decir que este programa suma dos números que hay en memoria.

Usamos las instrucciones en ensamblador en vez de en código máquina para que resulte más fácil de entender y porque, al mismo tiempo, es lo más cercano a la máquina.

Estado 1: programa cargado en memoria

  • La CPU tiene en su registro PC la dirección de la primera instrucción a ejecutar.
  • En la memoria tenemos las instrucciones en la sección .text y los datos a sumar en la sección .data.

cpu_memoria_ejecucion_estado1.png

Figure 6: Ejecución de programa en la CPU

Estado 2: primera instrucción en al CPU

  • La CPU tiene en el RI la instrucción a ejecutar y en el PC la dirección de la siguiente instrucción a ejecutar.

cpu_memoria_ejecucion_estado2.png

Figure 7: Ejecución de programa en la CPU

Estado 3: primera instrucción ejecutada

  • Una vez ejecuta la primera instrucción, tenemos en el registro edx el dato que había en memoria.

cpu_memoria_ejecucion_estado3.png

Figure 8: Ejecución de programa en la CPU

Estado 4: segunda instrucción en la CPU

  • La CPU tiene en el RI la segunda instrucción a ejecutar y en el PC la dirección de la siguiente instrucción a ejecutar.

cpu_memoria_ejecucion_estado4.png

Figure 9: Ejecución de programa en la CPU

Estado 5: segunda instrucción ejecutada

  • Una vez ejecuta la segunda instrucción, tenemos en el registro eax el dato que había en memoria.

cpu_memoria_ejecucion_estado5.png

Figure 10: Ejecución de programa en la CPU

Estado 6: tercera instrucción en la CPU

  • La CPU tiene en el RI la tercera y última instrucción a ejecutar y en el PC la dirección de la siguiente instrucción a ejecutar (no hay más, así que terminaremos aquí).

cpu_memoria_ejecucion_estado6.png

Figure 11: Ejecución de programa en la CPU

Estado 7: tercera instrucción ejecutada

  • Una vez ejecuta la tercera instrucción, tenemos en el registro eax el resultado de sumar lo que había en edx y en el propio eax.
  • Para la ejecución de esta instrucción, al ser una suma, se ha hecho uso de la ALU. Recuerda que la ALU hace las operaciones matemáticas y lógicas.

cpu_memoria_ejecucion_estado7.png

Figure 12: Ejecución de programa en la CPU

Author: Román Ginés Martínez Ferrández

Created: 2026-01-06 mar 18:17

Validate