Siguiente: Notas finales Superior: GNU as como ensamblador Anterior: ¿Por qué GNU as? |
Pues sí, porque en principio las herramientas de desarrollo servirán sólo para cuando el GNU as se utilice en modo nativo, y no cuando lo utilicemos para algún propósito particular. Explicaré esto en más detalle algo más adelante.
Pero vayamos al asunto que nos ocupa: usar GNU as como ensamblador de propósito general en una plataforma i386.
Por lo pronto, vamos a suponer que tenemos GNU as configurado para generar código para el objetivo elf-i386, que suele ser lo usual en plataformas PC y que es por otro lado el objetivo que nos interesa precisamente. Pues bien, esto significa que sin hacer nada nuestro GNU as ya genera código elf-i386 por defecto, o lo que viene a ser lo mismo en Linux/PC, que genera código en 32 bits para 80386 o superiores, en modo protegido. Esto no quiere decir en absoluto que el modo protegido sea 'de 32 bits'. El modo protegido implica, entre otras muchas cosas, separación entre tareas en código y en datos, pero no tamaño de operandos ni espacio de direccionamiento. El 80286 tenía un modo protegido con direccionamiento máximo de 24 bits, no de 32, por ejemplo. Bien, ya tenemos uno de los tres modos de la plataforma i386 cubierto. Veamos qué pasa con los otros.
Hay alguna más, pero las que nos interesan son esas. En particular, la limitación de direccionamiento de memoria de 16 bits y no 32 como cabría esperar. En realidad se pueden usar las mismas combinaciones de operandos de direccionamiento en las instrucciones que en modo protegido, sólo que
Hay formas de direccionar en 32 bits con el microprocesador en modo real, pero se sale del tema de este seminario.
Pese a que tengamos un i386 estará limitado, pero no hay que olvidar que sigue siendo un i386, con sus instrucciones extendidas, etc... La única limitación que nos importa es esa que implica el término 16 bits.
Desgraciadamente, poco o nada sabe nuestro GNU as de lo limitado de un i386 en modo real. Para él, el i386 está siempre en modo protegido, es decir, que permite direccionamiento en 32 bits. Esto significa que los operandos de las instrucciones se supondrán de 32 bits y el direccionamiento se supondrá también en 32 bits, es decir, que podremos direccionar nada menos que 4Gb por segmento... Pero esto no es cierto en modo real, en dónde sólo podemos direccionar 1Mb+64kb de memoria en total...
Para nuestra fortuna, en las últimas versiones de binutils (siento no poder precisar la versión exacta a partir de la cual ocurre), el GNU as entiende una directiva que le dice que olvide eso de generar código en 32 bits y que lo genere en 16 bits. Estupendo, eso parece solucionar todos nuestros problemas. Esa directiva es la .code16. Incluso podemos insertar pedazos de código ensamblados en 32 bits en segmentos definidos con .code16, simplemente escribiendo .code32, el código 32 y .code16 para volver al código 16.
En cuanto a los operandos, por defecto serán de 8/16 bits mientras que los sufijos de las instrucciones sean b o w. Si el sufijo fuese l se generará el size prefix y después la instrucción, con lo que el i386 en modo real sabrá que la instrucción usa operandos en 32 bits y la interpretará correctamente. Los problemas no van a venir por ahí...
En cualquier caso, no es problema, ya que se puede preceder cada direccionamiento de memoria que queramos hacer como si fuese en 32 bits de la directiva .addr32. Esta directiva obliga al GNU as a generar, en segmentos .code16 (segmentos con direccionamiento de 16 bits) el llamado 'address prefix', que le dice al i386 que interprete esa instrucción con direccionamiento de 32 bits. Asimismo, existe la directiva .addr16 para lo inverso.
Llegados a este punto, hemos escrito nuestro código de tal forma que está en un segmento (o sección) .code16, hemos intentado no abusar de los operandos de 32 bits y del direccionamiento en 32 bits (ambos añaden un prefijo de 1 byte y en determinados casos esto puede ser un desperdicio de espacio), y, en fin, hemos hecho un programa. ¿Qué viene ahora? Muy simple, ensamblarlo. Asumiremos que nuestro programa se llama test.s y que queremos generar test.o:
as --fatal-warnings -o test.o test.s
Pues bien, ya tenemos un código objeto en formato elf-i386... que nos es totalmente inútil. Lo que nosotros queremos es generar un programa que se ejecute en modo real, y ciertamente test.o no lo es... Pero no desesperemos. Aquí llega para ayudar el programa objcopy. Este programa es capaz de copiar código objeto de un formato a otro, y lo único necesario para que nos permita convertir nuestro elf-i386 a código en modo real es que nuestro objcopy soporte el objetivo binary. Haremos lo siguiente:
objcopy -v -j .data -O binary test.o
Ahora test.o debería contener exclusivamente el código para ejecutar en modo real. Pero ¿qué es eso de -j .data?, preguntará el avispado lector. Muy simple: binutils trabaja con secciones, que son, a todos los efectos, como segmentos de código. Si bien es posible crear un programa con GNU as que tenga múltiples secciones, no he tenido oportunidad de probarlo, y no es en absoluto necesario. Nosotros vamos a asumir que nuestro programa sólo tiene una sección, y va a ser la sección .data. ¿Por qué? pues muy simple: así no será tratada como código por ninguna utilidad que pudiere no entender el objetivo binary, sino que será considerada como datos y no se modificará. Hay que decirle pues a objcopy que copie sólo la sección .data. Se dirige al lector al manual del binutils para entender el funcionamiento y aplicación de las secciones.
Ya tenemos un código listo para ejecutarse perfectamente en modo real. Existe un Master Boot Record que se ha desarrollado siguiendo esta técnica y que será publicado en breve bajo licencia GPL.
Al igual que en el modo real, podemos usar direccionamiento con sintaxis de 32 bits, es decir, con las combinaciones extendidas de registro base+registro índice del i386, pero con cuidado de que la suma total del desplazamiento no exceda 64kB por segmento, o provocaremos una excepción.
Por otro lado, la generación del binario es como en modo real. (ver sec 4.1)
Esto, por suerte o por desgracia (según quien opine) no es un error, sino el comportamiento obvio de binutils. Esperemos, aún así, que en futuras versiones se amplíe este comportamiento.