Unidad 2. De lenguaje de alto nivel a código ejecutable
Introducción
En este punto del curso ya tienes una buena idea de cómo funciona un computador. También escribiste algunos programas en lenguaje ensamblador y estudiaste la relación entre este y el lenguaje de máquina.
A partir de ahora vas a emprender un viaje por varios de los componentes de software relacionados con los sistemas de cómputo. En esta unidad vas a aprender un nuevo lenguaje de programación muy relacionado con la programación a nivel de sistema.
Propósito de aprendizaje
Aprender un nuevo lenguaje de programación que te acerque a los conceptos de los sistemas de cómputo más naturalmente: lenguaje C.
Evaluación
Truco
RESULTADO DE APRENDIZAJE DEL CURSO
¿Recuerdas el resultado de aprendizaje del curso?
Aplico los conceptos necesarios para el correcto diseño, implementación,
funcionamiento y diagnóstico del software en la producción de sistemas de
entretenimiento digital utilizando los procedimientos y herramientas
adecuadas según el contexto. NIVEL RESOLUTIVO.
La idea de esta evaluación es que evidencies que eres capaz de aplicar los conceptos estudiados en la unidad anterior y esta mediante la construcción de una aplicación interactiva. Se espera que puedas realizar:
Lectura de comandos de entrada mediante teclado.
Lectura de archivos en memoria usando el HEAP.
Procesamiento de la información y manipulación de la memoria.
Escritura de archivos.
No solo se espera que apliques los conceptos para construir la aplicación, sino también para resolver los problemas (errores) que se presenten al construir la aplicación.
Enunciado
Vas a construir una aplicación interactiva de terminal. La aplicación
servirá para extraer información de una base de datos (Pokemon.csv).
Aquí está la base de datos.
En el archivo Pokemon.csv está la base completa, pero puedes encontrar
en archivos separados cada generación de pokémons.
Los requisitos de la aplicación son:
La aplicación funciona en un loop infinito esperando comandos desde la terminal.
Si escribes el comando
exitla aplicación termina.El comando
load archivocarga la base de datos en memoria. Cada registro (fila) de la base de datos se debe cargar en el HEAP como una estructura al igual que la estructura que representa como tal a la base de datos.El comando
sizereporta la cantidad de registros de la base de datosEl comando
range nmuestra losnprimeros registros de la base de datos.El comando
show nmuestra el registro con IDnEl comando
search stat valuebusca todos los registros cuyo value en el stat es el mismo y los va almacenado en unaLISTA DINÁMICAen el HEAP. Por ejemplosearch Generation 1busca todos los pokémons de generación 1.El comando
show searchmuestra la última lista generada con el comando search.El comando
save namesalva en un archivo la última lista generada. El nombre del archivo seráname.
Entrega
Entrega la evaluación en este repositorio con tu equipo de trabajo.
Debes incluir SOLO los archivo .c y .h con tu solución.
Debes incluir un archive README.md con:
Nombres y IDs.
¿Cómo se compila tu aplicación?
¿Cómo se ejecuta tu aplicación?
Un enlace a un video con tu aplicación funcionando.
Una explicación que diga cómo solucionaste cada problema.
Trayecto de actividades
Ejercicios
Para la realización de los ejercicios de la unidad te voy a crear este repositorio donde puedas experimentar tu solo. Recuerda que la evaluación de la unidad tendrá su propio repositorio. USA CODESPACES.
Ejercicio 1: retrieval practice
En la unidad anterior programaste en el lenguaje ensamblador de una CPU específica. Viste cómo traducir de lenguaje ensamblador a lenguaje de máquina. También te mostré algunas traducciones de lenguaje C a lenguaje ensamblador. Te voy a pedir que lo recuerdes, mira:
MEMORY[16] = 16384;
while (true)
{
if (MEMORY[KEYBOARD] == 0)
{
if ((MEMORY[16] - 16384) > 0)
{
MEMORY[16] = MEMORY[16] - 1;
MEMORY[MEMORY[16]] = 0x0000;
}
}
else
{
if ((MEMORY[16] - 24576) < 0)
{
MEMORY[MEMORY[16]] = 0xFFFF;
MEMORY[16] = MEMORY[16] + 1;
}
}
}
Este programa escrito en C puede tener una traducción a lenguaje ensamblador de la CPU que simulaste en la unidad anterior así:
Dirección |
Código ensamblador |
|---|---|
0 |
@16384 |
1 |
D=A |
2 |
@16 |
3 |
M=D |
4 |
@24576 |
5 |
D=M |
6 |
@19 |
7 |
D;JNE |
8 |
@16 |
9 |
D=M |
10 |
@16384 |
11 |
D=D-A |
12 |
@4 |
13 |
D;JLE |
14 |
@16 |
15 |
AM=M-1 |
16 |
M=0 |
17 |
@4 |
18 |
0;JMP |
19 |
@16 |
20 |
D=M |
21 |
@24576 |
22 |
D=D-A |
23 |
@4 |
24 |
D;JGE |
25 |
@16 |
26 |
A=M |
27 |
M=-1 |
28 |
@16 |
29 |
M=M+1 |
30 |
@4 |
31 |
0;JMP |
Identifica en el código ensamblador la inicialización de una variable.
Identifica en el código ensamblador una estructura IF/ELSE.
Identifica en el código ensamblador una while.
Ejercicio 2: de lenguaje de alto nivel a código ejecutable
Ahora te voy a mostrar cómo puedes hacer la traducción anterior, pero esta vez para la CPU de tu computador y usando herramientas (toolchain) específicas.
Advertencia
ESTE EJERCICIO ES DIDÁCTICO
En este ejercicio te muestro los pasos para que observes cómo funciona el proceso, PERO no es una manera práctica de trabajar. Al final te muestro cómo puedes hacer el proceso de traducción de forma más práctica.
Vamos a explorar juntos esta pregunta:
¿Cómo llegamos del código fuente en C al binario (unos y ceros, el ejecutable)?
En el caso del lenguaje C se siguen unos pasos conocidos como el pipeline de compilación compuesto por 4 pasos: preprocesamiento, compilación, ensamblado y enlazado.
Nota
IMPORTANTE
Para poder conseguir un ejecutable desde el código fuente, es necesario que nuestro código pase por todas las etapas del pipeline de manera exitosa.
¿Qué significa para ti la afirmación anterior?
Para ilustrar el proceso vamos a crear un programa compuesto por 3 archivos: dos archivos .c y un archivo .h. Todos los archivos estarán almacenados en el mismo directorio.
min.h
#ifndef MIN_H
#define MIN_H
int min(int, int);
#endif
min.c
#include "min.h"
int min(int a, int b){
if(a < b) return a;
else return b;
}
main.c
#include "min.h"
#include <stdio.h>
int main(int argc, char* argv[]){
printf("the min value is: %d\n",min(1,2));
return 0;
}
La idea será crear un ejecutable partiendo de estos tres archivos.
Ten presente que los archivos .h se usan para informarle al compilador
qué tipo de datos recibe la función min y qué tipo de dato devuelve. Los
archivos .h no se compilan, solo los archivos .c.
Iniciemos entonces el proceso con min.c:
Preprocesamiento:
gcc -E min.c. Al ejecutar este comando nota como el preprocesador incluye la información de min.h a min.cCompilación: ejecuta el comando
gcc -S min.c. La opción-Sindica quegccdebe hacer el proceso de preprocesador y con la salida de este paso se alimenta al compilador y detenerse en ese punto. El archivo de salida generado serámin.sque contendrá el código ensamblador. ¿Notas que el lenguaje ensamblador de la CPU de tu computador es diferente al de la unidad anterior?
.file "min.c"
.text
.globl min
.type min, @function
min:
.LFB0:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl -4(%rbp), %eax
cmpl -8(%rbp), %eax
jge .L2
movl -4(%rbp), %eax
jmp .L3
.L2:
movl -8(%rbp), %eax
.L3:
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size min, .-min
.ident "GCC: (Ubuntu 9.3.0-10ubuntu2) 9.3.0"
.section .note.GNU-stack,"",@progbits
.section .note.gnu.property,"a"
.align 8
.long 1f - 0f
.long 4f - 1f
.long 5
0:
.string "GNU"
1:
.align 8
.long 0xc0000002
.long 3f - 2f
2:
.long 0x3
3:
.align 8
4:
Ensamblado: en esta fase se genera el código máquina.
as min.s -o min.o. También es posible generar el código de máquina con el comandogcc -c min.cDebemos repetir este proceso con todos los archivos
.cde nuestro proyecto:gcc -c main.c. Ten presente que el comando anterior ejecutará automáticamente todos los pasos previos, es decir, el preprocesado, la compilación y el proceso de ensamblado.
Nota
HASTA AQUÍ YA TIENES TODOS LOS .o
Mira, si generar el ejecutable fuera como hacer una torta, en este momento ya tienes todos los ingredientes (archivos .o) listos para que los revuelvas (el proceso de enlazado).
Enlazado: una vez tengas todos los archivos
.olo último que debes hacer es enlazar todos los archivos para generar un archivo ejecutable. Este archivo contiene el código de máquina de todos los.opero organizado en un formato específico. En el caso de Linux el formato típico es.ELF. Ejecuta el siguiente comando para enlazar:ld min.o main.o. Verás el siguiente resultado:
ld: warning: cannot find entry symbol _start; defaulting to 0000000000401000
ld: main.o: in function main:
main.c:(.text+0x31): undefined reference to printf
Este resultado indica que no fue posible generar el ejecutable
(main.c:(.text+0x31): undefined reference to printf). Pero ¿Por qué?
la razón es que nos falta el archivo con el código de máquina de la función printf.
Esta función está prototipada en el archivo de cabecera (stdio.h), pero el archivo
no contiene el código fuente de printf. ¿Y dónde está el código entonces? este
código hace parte de la biblioteca glibc
que debes tener en tu sistema operativo y que contiene el código de máquina de varias
funciones, entre ellas, printf.
Una forma fácil de generar el ejecutable es utilizar de nuevo gcc. Este comando
se encargará de suministrarle a ld todo los archivos con código máquina necesarios para
generar nuestro ejecutable: gcc min.o main.o -o main.
Advertencia
MANERA PRÁCTICA DE HACER LA TRADUCCIÓN
Como te comenté al comienzo, una manera más práctica de hacer todo el proceso anterior es con este comando:
gcc -Wall main.c min.c -o main
Nota que el comando gcc se encargará de llamar todos
los programas por ti. Inicialmente le dices a gcc cuáles son los archivos
.c de tu programa y con la opción -o cómo se llamará el ejecutable. La opción
-Wall le dice a gcc que te muestre TODAS la advertencias.
No olvides que cada que cambies un archivo DEBES volver a llamar gcc para repetir todo el proceso de traducción.
Finalmente para ejecutar el programa:
./main
Observa que no estoy colocando main.c. Nota que main.c es uno de los
archivos de código fuente, no es el ejecutable. De hecho el ejecutable
no tiene extensión. ¿Ves la diferencia? y si en vez de main llamas al ejecutable
con el nombre ejecutable ¿Hay alguna diferencia? ¿Tendrías que cambiar algo
cuando llames a gcc?
Ejercicio 3: retrieval practice (evaluación formativa)
Realiza un diagrama mostrando los pasos necesarios para traducir un programa escritor en lenguaje C compuesto por 3 archivos .c, dos de los cuales tienen su archivo .h correspondiente. Muestra en tu diagrama que información entra y sale de cada paso.
¿Cuál es la diferencia entre preprocesamiento y compilación?
¿Cuál es la diferencia entre ensamblado y compilación?
¿Para qué sirve el proceso de ensamblado?
¿Cuál es la diferencia entre un error reportado en compilación y otro reportado en enlazado?
Ejercicio 4: retrieval practice (evaluación formativa)
En una sola línea de comando ¿Cómo puedes generar el ejecutable?
¿Qué debes hacer para compilar SOLO un archivo .c?
¿Por qué crees que puede ser útil compilar solo un archivo .c?
Ejercicio 5: para pensar
Inventa un programa que genere un error de compilación, es decir un error sintáctico.
Inventa un programa que genere un error semántico, es decir, compila pero no hace lo que se espera.
Inventa un error generado por el enlazador.
Advertencia
ERRORES DEL ENLAZADOR VS EL COMPILADOR
¿Cómo haces para diferenciar, observando la salida en terminal, un error de compilación vs un error en enlazado?
Ejercicio 6: lenguaje de programación C
Este será uno de los ejercicios más largos de la unidad porque te voy a mostrar
el lenguaje de programación con el cual estudiarás los conceptos que quedan
del curso: lenguaje C.
- Introducción a C
- Prerrequisitos
- Temas
- Lecturas, ejercicios y retos
- Lectura 1: ¿Qué es C y cuáles sus características principales?
- Lectura 2: Estructura básica de un programa en c
- Ejercicio 1: estructura de un programa en C
- Ejercicio 2: compilación de un programa en C
- Lectura 3: El preprocesador
- Lectura 4: Tipos de datos, tamaños y rangos
- Lectura 5: Variables
- Lectura 6: Ámbito y accesibilidad de las variables
- Ejercicio 3: ámbito y accesibilidad de variables
- Ejercicio 4: Operadores
- Lectura 7: Instrucciones de control de flujo
- Lectura 8: Punteros
- Reto 1: argumentos, punteros y funciones
- Lectura 9: Arreglos y su relación con los punteros
- Reto 2: arreglos, funciones, punteros
- Lectura 10: cadenas de caracteres
- Reto 3: arreglos, cadenas, punteros
- Lectura 11: memoria dinámica
- Lectura 12: estructuras de datos
- Reto 4: comparación de estructuras
- Lectura 13: estructuras y punteros
- Lectura 14: entrada/salida (teclado-pantalla)
- Ejercicio 5: ingresar números por teclado
- Ejercicio 6: convertir caracteres a números (forma 1)
- Ejercicio 7: convertir caracteres a números (forma 2)
- Ejercicio 8: imprimir con printf
- Lectura 15: entrada/salida (archivos)
- Ejercicio 9: leer un archivo hasta el final
- Reto 5: leer y escribir en un archivo
- Ejercicio 10: repaso de punteros
- Ejercicio 11: arreglos de arreglos
- Ejercicio 12: repaso de arreglos, punteros y funciones
- Reto 6: funciones, arreglos y punteros
- Ejercicio 13: repaso de archivos