¿Cómo funciona una computadora?: ¿Qué pasa cuando pulso la tecla A? – Práctica

En la primera parte expliqué qué sucedía en un ordenador cuando pulsabas una tecla A de una forma algo más genérica y teórica. En esta segunda parte intentaré describir un ejemplo algo más práctico para que lo comprendas de forma más intuitiva. Por supuesto con ejemplos desde Linux y con códigos en lenguaje de programación C como suelo venir haciendo…

Con el anterior artículo, si eres principiante en esto, obtendrás una visión más amplia de lo que sucede cuando presionas una tecla. Pero ahora toca demostrar con ejemplos prácticos lo que se cuece dentro de tu equipo cada vez que se realiza alguna acción. Para eso también me ayudaré de algunos códigos en ASM (ensamblador), comandos, etc., donde puedes visualizar in-situ lo que va ocurriendo…

Ejemplo práctico de lo que sucede en Linux

Ya deberías tener más o menos claro el sistema de E/S para un programa simple en C si has leído mis tutoriales de programación. Cuando hablo de entrada me refiero a que se puede alimentar el programa con algunos datos, como puede ser una pulsación de alguna tecla desde el teclado.

En cambio, la salida es lo contrario, se muestran datos en algún dispositivo de salida. Por ejemplo se manda a imprimir, o se muestra por la pantalla. Ya sabrás también que stdin (entrada estándar) en Linux es el teclado, y stdout (la salida estándar) es el monitor o pantalla.

Cuando en los ejemplos de Hola Mundo en C escribía printf(«Hola mundo\n»); no es más que otra forma de indicarle al ordenador fprintf ( stdout, «Hola mundo\n»); solo que en este segundo caso se usa el puntero hacia esa salida estándar de la que hablo.

En lenguaje C se podría jugar con varios ejemplos, como este sencillo para poder comprobar qué ocurre cuando presionas una tecla A:

#include <stdio.h>
int main()
{
     int tecla;
     printf("Presiona la tecla A:");
     tecla = getchar ();
     printf("\nHas pulsado: ");
     putchar (tecla);
     return 0;
}

Es un código simple en el que se te pide pulsar una tecla, en este caso A, y se te muestra en pantalla la tecla pulsada. Ahora toca compilarlo con gcc como te mostré en el tutorial dedicado a ello. Y al ejecutarlo verás el resultado, que sería algo así:

codigo c

Bien, pero esto no es lo que me interesa. Sería más interesante ver el código ensamblador correspondiente a este pequeño código (puedes usar la opción -S del compilador gcc), para ver qué necesita ejecutar la CPU. Dicho código para AMD64 o x86-64 o EM64T, como lo quieras llamar, sería:

	.file	"codigo.c"
	.text
	.section	.rodata
.LC0:
	.string	"Presiona la tecla A:"
.LC1:
	.string	"\nHas pulsado: \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	$16, %rsp
	leaq	.LC0(%rip), %rdi
	movl	$0, %eax
	call	printf@PLT
	call	getchar@PLT
	movl	%eax, -4(%rbp)
	leaq	.LC1(%rip), %rdi
	call	puts@PLT
	movl	-4(%rbp), %eax
	movl	%eax, %edi
	call	putchar@PLT
	movl	$0, %eax
	leave
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE0:
	.size	main, .-main
	.ident	"GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0"
	.section	.note.GNU-stack,"",@progbits

De ese código no me interesa la mayor parte del  código, solo las instrucciones call. ¿Recuerdas cuando comenté en la primera parte teórica sobre las syscalls o llamadas al sistema? Si lo recuerdas, en Linux se pueden invocar llamadas al sistema desde el código ensamblador de varias formas como con la instrucción int 0x80, como también con syscall, y con call como en este caso. La interrupción int es más antigua para x86, mientras que desde x86-64 se introdujo la instrucción nueva syscall. Y ahora también se puede usar la biblioteca call. En éste último caso se pueden llamar así a funciones, pero es algo más complejo, de hecho, si te fijas usa un PLT (Procedure Linckage Table), es decir, una forma de apuntar a una sección pequeña dentro de un proceso…

Si te centras en el código tecla = getchar(); correspondería a la siguiente porción de ASM:

call getchar@PLT 
movl %eax, -4(%rbp)

Pero es algo más complejo de ver por esta biblioteca (me refiero a call). Pero piensa que se está usando la biblioteca stdio.h de C, y dentro están definidas las funciones como printf (imprime a través de stdout), putchar (imprime caracteres en la stdout), getchar (obtiene caracteres de la stdin) que se han usado. Todo eso necesita de una serie de procesos de hardware y recursos que no puede manejar el propio programa, sino que lo debe gestionar el kernel como dije en el artículo anterior. Es en este snippet de código donde se realizaría dicha petición…

Recuerda que con el comando ldd ./codigo puedes ver las bibliotecas que usa este código, si le has llamado «codigo» como en mi caso…

Otra prueba que se puede hacer es ejecutar el programa y en otra ventana del terminal ejecutar esto para obtener información del proceso correspondiente a nuestro programa:

ps aux | grep codigo

Y la salida que me da en mi caso es:

isaac     6960  0.0  0.0   4504   720 pts/0    S+   12:54   0:00 ./codigo

Es decir, que el PID=6960, que pertenece a mi usuario isaac, que está esperando a un evento y que está en primer plano (S+), hora, etc. Con el PID se puede obtener más información que es a lo que voy. Por ejemplo, puedes ir a:

cd /proc/6960
ls

Allí contarás toda la información sobre este proceso, incluída alguna muy interesante de la memoria. ¿Como qué? Pues por ejemplo:

  • /proc/6960/cmdline: si usas el concatenador (cat) con este fichero podrás ver el nombre del programa. En este caso sería codigo.
  • /proc/6960/cwd: enlace simbólico al directorio de trabajo del proceso.
  • /proc/6960/environ: contiene nombres y variables de entorno que afectan a este proceso.
  • /proc/6960/exe: enlace simbólico hacia el ejecutable original.
  • /proc/6960/fd: directorio con enlace simbólico para el descriptor de archivo o FD.
  • /proc/6960/fdinfo: directorio de entradas que describen la posición y flags para el FD.
  • /proc/6960/maps: contiene información sobre el mapeo de ficheros y bloques.
  • /proc/6960/mem: binario que representa la memoria virtual para el proceso.
  • /proc/6960/status: información del estado del proceso y uso de memoria.
  • /proc/6960/task: enlace duro hacia cualquier tarea necesaria.

Realmente toda esta información te da una idea de cómo está tratando la CPU al proceso, así como los descriptores y el uso de la memoria para este proceso. Quizás esto tampoco te dice mucho como ocurría con el ASM, pero piensa en lo que te comenté en la parte teórica de este artículo cuando mostré lo de stdin, stdout, los ficheros /dev/hidrawX, etc., y comienza a atar cabos complementándolo con esta otra parte…

Continúo para que puedas «ver» cosas sobre este código para saber mejor lo que está ocurriendo en tu ordenador. Y strace puede ser una buena herramienta para seguir indagando:

#Durante la ejecución del proceso codigo, puedes usar desde otra ventana del terminal
sudo strace -p 6960

Eso te puede aportar interesante información del proceso y lo que va sucediendo. Con una salida similar a:

strace: Process 6960 attached
read(0, "a\n", 1024)                    = 2
write(1, "\nHas pulsado: \n", 15)       = 15
write(1, "\n", 1)                       = 1
write(1, "a", 1)                        = 1
lseek(0, -1, SEEK_CUR)                  = -1 ESPIPE (Illegal seek)
exit_group(0)                           = ?
+++ exited with 0 +++

En este caso, la segunda linea se quedará a la espera de que pulses alguna tecla. Y al pulsar A se completa la línea y luego aparece el resto de líneas correspondientes a la imrpesió o escritura del mensaje del printf y mostrar la letra, etc. Si te fijas, write y read son llamadas al sistema. Lo que le sigue entre paréntesis son los argumentos pasados a la llamada y el número = x sería el valor retornado por la syscall.

Otra opción es usar estas otras opciones:

sudo strace -c -p 6960

Con una salida como:

strace: Process 6960 attached
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
  0.00    0.000000           0         1           read
  0.00    0.000000           0         3           write
  0.00    0.000000           0         1         1 lseek
------ ----------- ----------- --------- --------- ----------------
100.00    0.000000                     5         1 total


En este caso es un sumario del proceso, con el tiempo empleado, la cantidad de llamadas y el tipo.

Si quieres ver el puntero de instrucciones, puedes usar esta otra opción:

sudo strace -i ./codigo

Con una salida tipo:

[00007f5532f4de37] execve("./codigo", ["./codigo"], 0x7ffc7d8149f8 /* 17 vars */) = 0
[00007fec2887eec9] brk(NULL)            = 0x5610f5ebb000
[00007fec288727de] access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
[00007fec2887fe27] access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
[00007fec2887fcdd] openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
[00007fec2887fc43] fstat(3, {st_mode=S_IFREG|0644, st_size=115158, ...}) = 0
[00007fec2887ff43] mmap(NULL, 115158, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fec28a6d000
[00007fec2887fed7] close(3)             = 0
[00007fec2887b139] access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
[00007fec2887fcdd] openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
[00007fec2887fda4] read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\260\34\2\0\0\0\0\0"..., 832) = 832
[00007fec2887fc43] fstat(3, {st_mode=S_IFREG|0755, st_size=2030544, ...}) = 0
[00007fec2887ff43] mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fec28a6b000
[00007fec2887ff43] mmap(NULL, 4131552, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fec28472000
[00007fec2887fff7] mprotect(0x7fec28659000, 2097152, PROT_NONE) = 0
[00007fec2887ff43] mmap(0x7fec28859000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1e7000) = 0x7fec28859000
[00007fec2887ff43] mmap(0x7fec2885f000, 15072, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fec2885f000
[00007fec2887fed7] close(3)             = 0
[00007fec28864024] arch_prctl(ARCH_SET_FS, 0x7fec28a6c4c0) = 0
[00007fec2887fff7] mprotect(0x7fec28859000, 16384, PROT_READ) = 0
[00007fec2887fff7] mprotect(0x5610f4cc3000, 4096, PROT_READ) = 0
[00007fec2887fff7] mprotect(0x7fec28a8a000, 4096, PROT_READ) = 0
[00007fec2887ffd7] munmap(0x7fec28a6d000, 115158) = 0
[00007fec285817c3] fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 2), ...}) = 0
[00007fec285884b9] brk(NULL)            = 0x5610f5ebb000
[00007fec285884b9] brk(0x5610f5edc000)  = 0x5610f5edc000
[00007fec285817c3] fstat(0, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 2), ...}) = 0
[00007fec28582154] write(1, "Presiona la tecla A:", 20Presiona la tecla A:) = 20
[00007fec28582081] read(0, a
"a\n", 1024) = 2
[00007fec28582154] write(1, "\nHas pulsado: \n", 15
Has pulsado: 
) = 15
[00007fec28582154] write(1, "\n", 1
)    = 1
[00007fec28582154] write(1, "a", 1a)     = 1
[00007fec28582217] lseek(0, -1, SEEK_CUR) = -1 ESPIPE (Illegal seek)
[00007fec28556e06] exit_group(0)        = ?
[????????????????] +++ exited with 0 +++



Esto lo que muestra es cada llamada realizada por el programa, con el instruction pointer (registro PC ¿recuerdas?, siente instrucción a ejecutar…) correspondiente. Es decir, lo que aparece al comiento son direcciones de memoria donde se apunta para ejecutar esto.

Puedes incluso filtrar para solo ver información de una llamada específica, por ejemplo:

sudo strace -e trace=read

Por cierto, si recuerdas el artículo sobre el depurador gdb, también te podría dar datos interesantes de lo que va sucediendo…

Isaac

Apasionado de la computación y la tecnología en general. Siempre intentando desaprender para apreHender.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

A %d blogueros les gusta esto:

Si continuas utilizando este sitio aceptas el uso de cookies. más información

Los ajustes de cookies de esta web están configurados para "permitir cookies" y así ofrecerte la mejor experiencia de navegación posible. Si sigues utilizando esta web sin cambiar tus ajustes de cookies o haces clic en "Aceptar" estarás dando tu consentimiento a esto.

Cerrar