next up previous
Siguiente: Notas finales Superior: GNU as como ensamblador Anterior: ¿Por qué GNU as?

Subsecciones


GNU as como ensamblador de propósito general

Como ya se apuntó anteriormente, el GNU as tiene una ventaja importante a la hora de mezclar código escrito en un lenguaje de alto nivel con código escrito en lenguaje de ensamblado: genera el mismo formato que los demás compiladores (esto, por otra parte, resulta obvio: casi todos los compiladores lo que hacen es generar código en lenguaje de ensamble y ensamblar luego con el GNU as). Como añadidura, podremos usar todas nuestras herramientas tradicionales de desarrollo con el código generado por el GNU as. Qué maravilla... y qué limitación...

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.


GNU as como ensamblador para modo real

Empecemos con el modo real. En modo real, un i386 es como un 8086 sólo que mucho más rápido y con ciertas extensiones:

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...

¿Qué ocurre entonces?

Pues que si escribimos un código con GNU as, lo ensamblamos y posteriormente lo ejecutamos en modo real, no se comportará correctamente, ya que se interpretará de otra forma. No entraré en detalles sobre lo que ocurre pero basicamente se trata de que las instrucciones, en modo real, esperan operandos de 16 bits y no de 32, y puesto que el GNU as generará operandos de 32 bits, cada instrucción irá con una cola de 16 bits que se interpretará como otra instrucción. Oh, fatalidad...

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í...

Y ¿qué pasa con la memoria?.

Pues que el GNU as, en su implacable exactitud, no nos dejará usar direccionamiento extendido, por mucho que nosotros aseguremos que los desplazamientos son de 16 bits. Este direccionamiento, como en el 8086 consiste en tres partes: un registro base, un registro índice y un desplazamiento (un número...). La diferencia es que en el 8086 están bastante restringidas las combinaciones de estos tres elementos y no todos los registros pueden actuar como base, no todos los registros índice se pueden usar con todos los base, etc... Ciertas combinaciones de registro base+registro índice, y ciertos registros base son inválidos en código puro de 16 bits, es decir, son inválidos en un 8086. Pero resulta que en nuestro i386 ¡son perféctamente válidos!. ¿Por qué, entonces, el GNU as no quiere que los usemos?2. Pues mucho me temo que es una incongruencia del GNU as, que en 16 bits no quiere usar direccionamientos inválidos para el 8086 pero sí que nos deja usar códigos de operación extendidos del i386...

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.


GNU as como ensamblador para modo virtual86

Cuando usamos el modo virtual86 el microprocesador está realmente en modo protegido, pero emula un 8086 virtual, lo cual quiere decir que cuando usemos el GNU as tendremos que tener todas las precauciones que ya teníamos en modo real. Basicamente el modo virtual86 es un modo real dentro del modo protegido, o mejor aún, un i386 que direcciona como un 8086.

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)


Interacción con binutils

Para terminar, hemos mencionado anteriormente que se puede usar binutils con los códigos generados por el GNU as usado como ensamblador de propósito general, y esto no es cierto al 100%. Por lo general, debieran funcionar perfectamente, pero por lo menos en la versión 2.9.5 de binutils, el comando objdump y otros no entienden los códigos de operación que se generan en .code16, ya que ellos no disponen de directivas que les indiquen el tamaño por defecto de los operandos y de los direccionamientos.

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.



Notas al pie

... usemos?2
Parece, en cualquier caso que esta incongruencia se ha solucionado ya en la versión 2.10 del GNU as.

next up previous
Siguiente: Notas finales Superior: GNU as como ensamblador Anterior: ¿Por qué GNU as?

Download this document: [src.tar.gz][ps.gz][html.tar.gz][dvi.gz]

Congreso HispaLinux 2000