Los misterios de C, el lenguaje inmortal.

Soy un enamorado de C

Soy un enamorado de C, lo reconozco, un joven con actitud old school, simpatizante de increpar a los amantes de Java, Python o C# por su vida sencilla y despreocupada. 

Si eres un habitual del desarrollo de software para sistemas embebidos o alguna vez has indagado un poco en el kernel de un sistema operativo, seguro que te habrás dado cuenta del enorme papel que ha jugado, esta jugando y jugará el anciano lenguaje C. 

Desde su nacimiento en 1972, este lenguaje se ha mantenido como uno de los lenguajes más importantes y más utilizados a nivel mundial. A pesar de sus ya 50 años de vida y la aparición de nuevos lenguajes tan venerados como Java, Python o C#, el viejo C sigue defendiéndose sin demasiados problemas. Él, junto con heredero C++ tienen recorrido aún por muchos años. 

Te contaré sus secretos.



La pregunta que trataré de resolver es la siguiente: 

¿Qué sentido tiene que se utilice todavía un lenguaje como C que es engorroso y muy difícil de dominar existiendo lenguajes tan molones y agradecidos como Python o Java?. ¿Acaso nos gusta ser unos mártires y complicarnos la vida tontamente solo para vacilar de ser unos puristas?. ¿Acaso somos masocas?


Aunque lo de ser un purista sí que es mi rollo, no es la respuesta. Veamos realmente que hace diferente a C de otros lenguajes, cual es su importancia en el mundo de la computacion y porque deberías aprenderlo ya mismo si quieres desarrollarte como ingeniero de sistemas embebidos, desarrollador de kernels, desarrollador de protocolos de comunicaciones, drivers, middleware, etc.


Parte 1.  Acceso a memoria. 

Para hablar de la importancia del acceso a memoria, tal y como lo vamos a ver a continuación es imprescindible dar un pequeño rodeo y hablar primero de uno de los tres pilares fundamentales del funcionamiento de cualquier computador, los REGISTROS.


Los registros

Los llamados registros son el corazón de cualquier sistema digital, su alma mater, lo que convierte a un sistema de computación en un sistema de computación PRO-GRA-MA-BLE.

Los registros son unidades de memoria que permiten almacenar datos en forma de unos y ceros (voltaje alto y voltaje bajo respectivamente) y están presentes en absolutamente todos los computadores. Aunque los dispositivos de memoria que comunmente conocemos como la memoria flash, la memoria  ram, memoria rom, etc,. están compuestas de registros, no son los únicos dispositivos que disponen de estos elementos, por ejemplo en el interior de cualquier procesador encontramos también miles de estos compomentes, aunque con una funcionalidad diferente.

Mientras que en una memoria los registros son de uso general y sirven para almacenar datos (programas, canciones, hojas Excel, etc.), en el caso de los registros que forman parte de un procesador o un microcontrolador, su utilidad es la de modificar directamente el comportamiento del mismo y programar su funcionamiento.

Si vemos por ejemplo un registro de 8 bits (ocho unos o ceros) y lo asemejamos a una autopista con 8 carriles, siendo los coches la electricidad que circula, podemos controlar el trafico mediante peajes que dejan o no pasar a los coches de cada carril (nuestros registros). En el caso de que en un peaje exista un 1 los coches podrán pasar, pero si en su lugar hay un 0 los coches de ese carril se quedaran parados. Como un procesador esta compuesto por miles de carriles (circuitos) que interconectan peajes (registros), lo que hacemos al modificar los registros es modificar los circuitos por los que circula corriente en nuestro procesador. 

Así, a través de estructuras electrónicas construidas con estos registros, en los cuales podemos modificar su valor mediante los lenguajes de programación, podemos controlar el funcionamiento de nuestro computador ya sea para decirle que la siguiente operación es una suma, una división, un ciclo de reloj, el trigger para despertar un periférico, rellenar el registro del buffer de un puerto Ethernet, la encriptación de una secuencia de bytes,  etc, etc, etc…  

En resumen. Cualquier tipo de lenguaje de programación existente sobre cualquier computador existente se basa en última instancia en la modificación de los registros internos del mismo.

 Un caso de programación podría ser por ejemplo:

    int x1 = 10;

    int x2;

    x2 = x1;

    int x3 = 30;

    x2 = x2 + x3;

Que traducido sería algo así como “copia (registro Y1) el valor guardado en la dirección de memoria X1 (registro x1) en la dirección de registro X2 (registro X2)” y “suma (registro Y2) al valor guardado en la dirección X2 (registro X2) al valor guardado en la dirección X3 (registro X3)”.

Terminada esta mini disertación sobre sistemas electrónicos programables, volvemos al tema y a la pregunta que quería responder realmente: ¿Qué hace tan especial al lenguaje C si todos los lenguajes de programación hacen lo mismo que, en definitiva, es actuar sobre los registros?


Lo que hace especial a C

El enunciado anterior de que todos los lenguajes actúan sobre los registros es una verdad pero una verdad a medias. Que todo lenguaje de programación acaba en última instancia modificando los registros del computador es cierto. Que todos los lenguajes te permitan modificar a tus anchas estos registros o por lo menos que sea igual de sencillo ya no lo es tanto. De hecho, una de las principales diferencias entre los lenguajes de bajo nivel como C o C++ y los lenguajes de alto nivel como Java o Python es que estos últimos restringen al usuario (por seguridad y comodidad) el acceso al hardware. Es decir, no es posible actuar directamente sobre los registros.

¿A que se debe? No es que los creadores de lenguajes de alto nivel como Java, Python, C#, etc, no fuesen capaces de crear un lenguaje que pudiese acceder a memoria directamente sino que, ciertamente no querian, no les interesaba. 

En estos lenguajes, cuyo objetivo es la eficiencia en el desarrollo y la facilidad de uso y aprendizaje, se  intenta por todos los medios eliminar los problemas que surgen al trabajar con estructuras hardware de bajo nivel (registros) debido a su dificultad, los enormes conocimientos que son necesarios y el riesgo que entrañaría para gente inexperta.

Así, estos lenguajes delegan estas tareas a capas software inferiores sobre las que corren, en particular a alguna forma de sistema operativo (o maquina virtual) cuya misión principal es la de implementar toda la lógica de acceso y manejo del hardware y de crear una frontera entre el usuario y el kernel (user space vs kernel space). 


Cuando un lenguaje como Java quiere mandar un mensaje por el puerto USB o crear un Timer o crear una tarea nueva, delega en el sistema operativo que se encarga de reconocer el hardware sobre el que trabaja y de manejarlo correctamente, de forma transparente para el usuario. 

El lenguaje C, de más bajo nivel, capaz de ejecutarse directamente sobre la maquina, si que tienen la capacidad de actuar sobre estos registros y modificar directamente el comportamiento del computador. Es decir, en el lenguaje C, una de sus caracteristicas principales es la facilidad con la que se acede a las direcciones de memoria de los registros y lo facil que es modificar sus valores. El solo se basta.

Este caracteristica de C es precisamente la más interesante para el desarrollo de capas de software en contacto directo con el hardware como por ejemplo los sistemas operativos. Cabe resaltar que las partes más críticas y fundamentales de la mayoria sino todos los sistemas operativos actuales (Unix, Windows, macOS, Android…) están programadas en C. 

Como anaecdota, el desarrollo de C y el de Unix se realizó en paralelo puesto que en C se iban plasmabando las caracteristicas deseadas para ser el lenguaje optimo de desarrollo de los nuevos sistemas operativos.

En este video el creador de Linux, Linus Torvalds habla del porqué del uso de C en cualquier sistema operativo. Linus Torvalds "Nothing better than C"

De este modo queda clara la importancia de C para el control de hardware frente a otros lenguajes como Python o Java. Apliquemoslo sobre nuesto campo, veamos su aplicación en los sistemas embebidos.


Aplicación de C en los sistemas embebidos.

Como imaginarás en el mundo de los sistemas embebidos, estos aparatejos tan minúsculos y especializados, diseñados con el objetivo de que nada sobre porque si algo sobra cuesta pasta y si cuesta pasta no mola, no todo iba a ser del color de rosa.

En la mayoría de los casos el sistema operativo de estos sistemas (si es que existe) no es tan ambivalente como uno de uso general ni con las mismas capacidades (necesitamos ahorrar recursos) por lo que el sobrecoste en rendimiento, memoria, y portabilidad que conllevaría correr lenguajes de alto nivel como Python queda descartado casi de inmediato.

Además, en el caso de que exista un mínimo sistema operativo, en más de una ocasión no satisfará al cien por cien nuestros requerimientos y uno tendrá que arremangarse y bajar hasta las entrañas más oscuras del mismo con el objetivo de crear nuevos módulos, modificar drivers, portarlo a nuevas plataformas hardware, etc, etc, etc,. algo que se realiza en C si o si pues, amigo, estás trabajando directamente con el hardware. 

Si no existe ni tan siquiera sistema operativo y practicas el llamada Bare-Metal programming, te encontraras modificando directamente los registros de tu microcontrollador/microprocesador y por tanto C volverá a estar presente.


¿Empieza a quedar claro por qué es y será tan complicado que otro lenguaje destrone al anciano C en aquello que tan bien se le da (manejo del hardware) y que tan básico y fundamental es en cualquier computador? Desde el mayor supercomputador existente, a los servidores más novedoso, tu smarwatch de última generación, tu móvil e incluso tu despertador, todos ellos necesitan manejar el hardware y es por ello que aun nos queda C para ratos.


Parte 2. Velocidad.

Cuando hablábamos de los sistemas embebidos hemos hablado de pasada de tres conceptos fundamentales en nuestro sistema que son el  acceso a memoria (como ya hemos visto), el rendimiento (velocidad del código) y la portabilidad. Analicemos el segundo, la velocidad y hablemos de porque C también es un referente en cuanto a ello.

La velocidad con la que se ejecuta una linea de ódigo es un concepto muy importante, no solo en los sistemas embebidos sino en todos y cada uno de los computadores existentes. Esta búsqueda de la maxima velocidad es feroz cuanto más descendemos en las capas software de un sistema. Así, aunque podemos permitirnos que una aplicación de alto nivel como por ejemplo whatsapp sea un poco lenta, es completamente inimaginable que un componente del sistema operativo (pegado al hardware) y que se invoca cientos de miles de veces por todas las capas superiores sea lenta pues bloquearia todo el sistema.

Sucede que cada lenguajes de nueva generación añade una capa nueva (capa de abstraccion) por encima de los lenguajes de generaciónes anteriores. Los beneficios son claros, mayor abstraccion, facilidad y velocidad de desarrollo junto con menos complejidad para el desarrollador y menos riesgo de cargarte el sistema, etc. Sin embargo como consecuencia negativa, su eficiencia, su velocidad disminuyen pues más y más capas tienen que atravesar hasta que se llega finalmente al hardware (registros). 

De este modo, cada linea de codigo escrita en un lenguaje de mayor nivel multiplica sus lineas de codigo cada vez qa medida que es convertido a codigo de un nivel inferior. Asi por ejemplo: 1 linea de Python facilmente se convierte en 100 lineas de C que se convierte en 10000 de lineas de ensamblador que se convierte en 1000000 instrucciones de codigo maquina.




El lenguaje C, dado que se encuentra en capas inferiores de abstraccion (más pegadito al hardware), convierte cada una de sus instrucciones en un bajo numero de lineas de codigo maquina (codigo binario que directamente actua sobre los registros), convirtiendose en uno de los lenguajes (independiente del codigo maquina) más rapidos del mundo. 

Nota: A veces, en operaciones que requieren eficiencia extrema se puede llegar a usar el lenguaje ensamblador que todavia es un nivel menor de abstracción que C pero este lenguaje es completamente dependiente del tipo de arquitectura y su portabilidad de un sistema a otro se ve gravemente mermada.

Queda patente pues que en sistemas tan especializados como los sistemas embebidos, el kernel de los sistemas operativos, protocolos de comunicacion de bajo nivel, el middleware, etc la eficiencia que aporta el lenguaje C hace que su uso siga sindo tan elevado.


Parte 3. Portabilidad.

Como anteriormente hemos comentado la funcionalidad ultima de cualquier lenguaje de programación es modificar los registros del computador para realizar las tareas deseadas. Estamos acostumbrados a hacer funcionar software como un sistema operativo en diversos computadores sin darnos cuenta siquiera las diferencias en la arquitectura del hardware con el que estan construidas.

Un ejemplo de ello es que el sistema opertativo Linux o Windows y todas las aplicaciones desarrolladas para ellos pueden ejecutarse facilmente sobre una arquitectura ARM (raspberry pi) o x86 (tipico de sobremesa) sin modificación alguna, sin darnos ni cuenta que la esencia del computador,  es decir los registros sobre los que actuamos, son completamente diferentes.

La portabilidad de un lenguaje como tal, representa la facilidad de crear codigo independiente del hardware, es decir, capaz de adaptarse a diferentes arquitecturas. Con ello, la importancia de que un lenguaje sea portable es extrema pues nadie quiere tener que reescribir todo el codigo cada vez que cambias de computador.


Un poco de historia sobre los lenguajes de computacion. El origen.

En sus origenes, los primeros lenguajes de programación, consistian en escribir, directamente en binario registro por registro las acciones que realizabamos sobre el computador. Escribir estos programas conllevaba un labor titanica, su compresion era muy complicada y los errores humanos abundaban. Cada vez que un programa debía portarse a un computador con una arquitectura ligeramente diferente, todo el programa se había de reescribir. 

	                
  ¿Te imaginas tener que escribir un programa asi?

    01100010 10010100 

    01100011 01000010

    01100101 01010010

    01100110 01010011

Estos lenguajes se llamaban lenguajes maquina y durante los comienzos de la computación era la unica forma de programar los ordenadores. Por algo no existia Instagram.


Un paso hacia la luz. El lenguaje ensamblador.

Buscando una mayor facilidad de escritura de los programas, el lexico se aproximó hacia un lenguaje más natural y se desarrollo el lenguaje ensamblador. En el, cada arquitecutura definia una serie de comandos que representan instrucciones basicas del computador. Asi, mediante un comandos facil de entender y memorizar podiamos crear nuesto codigo y un programa externo llamado ensamblador era el encargado de convertir esos comandos en codigo maquina como el anterior, sin fallos y con un notorio aumento de velocidad de desarrollo. Comenzo a reutilizarse el codigo.

Mediante el lenguaje ensamblador se escribian programas de este tipo que automaticamente se convertian al codigo maquina como el del ejemplo anterior:

	        

   mov1 0xFF001122, %eax       
   
   add1 %eax, %edx
   
   xor1 %esi, %esi
   
   push1 %ebx

Aunque sigue siendo poco intuitivo respecto a los lenguajes actuales, es mucho más sencillo de manejar y de memorizar sus instrucciones y funcionamiento respecto al codigo maquina.

Aun así, el lenguaje ensamblador es un lenguaje fuertemente ligado al hardware subyacente pues todavia tenemos que tener muy claro que registro estamos modificando y como, lo que hace que su portabilidad siga siendo minima. Aunque escribamos un programa en ensamblador mas facilmente que en codigo maquina, si cambiamos de arquitectura las instrucciones cambian y por tanto seguimos sin tener portabilidad.


La luz. Los lenguajes independientes de la maquina. 

Con la imperiosa necesidad de reutilizacion completa del codigo y por ende la portabilidad entre plataformas, surgieron la tercera generación de lenguajes, los cuales sustentados sobre los dos anteriores, conseguian ser independientes del hardware subyacente y por tanto, portables. Surgen así lenguajes como FORTRAN, Cobol, B, C, etc.

Estos lenguajes, conocedores de todos las instrucciones en ensamblador de las distintas arquitecturas son capaces de autogenerar el codigo ensamblador especifico y por tanto, cambiando la arquitectura objetivo podemos usar el mismo codigo escrito en C para cada arquitectura que queramos. (No es tan trivial como parece pero esa es la esencia).

El lenguaje C como tal fue diseñado pensando especificamente en la maxima portabilidad. Los mecanismos que lo permiten y las capas de abstraccion utilizados para ello estan implementadas en un proceso del cual tanto hemos oido hablar, la compilación.


Del proceso de conversion del codigo C / C++ en lenguaje maquina (comunmente conocido como compilacion), así como de todos los programas que intervienen en el mismo (preprocessor, compiler, assembler, linker & loader) hablaremos con más detalle en futuras entradas.

El lenguaje C, en union con una serie de estandares de portabilidad (ANSI C e ISO C, de los cuales tambien hablaremos en futuras ocasiones) es, comparado con lenguajes como Java o Python, un lenguaje de bajo nivel (velocidad, eficencia y acceso a memoria) con capacidad plena de portabilidad, lo que lo hace idoneo para el desarrollo de software ligado con el hardware como kernels de sistemas operativos, drivers y casi el 99 porciento de todo el desarrollo de software para sistemas embebidos.


Resumen

El lenguaje C es un lenguaje veloz, eficiente, capaz de controlar directamente el hardware y portable, idoneo para aplicaciones donde estos propiedades sean clave como lo son el core de cualquier sistema operativo, sus drivers y por supusto la entera mayoria de los desarrollos software para sistemas embebidos.

Si alguna vez te has planteado como funcionan las entrañas del software de tu computador, sea del tipo que sea y se cual sea la aplicación para el que este destinado, ten por seguro que te vas a encontrar en más de una ocasion con el lenguaje C.

Comments

Popular posts from this blog

¿Qué es un sistema embebido?

Introducción a los sistemas multitarea

Patrones software en arquitecturas orientadas a control