ElBlo

jmp Bug

A principios del 2020, cuando la pandemia de coronavirus estaba empezando, me ofrecí a dar una mano respondiendo consultas a estudiantes de la materia Organización del Computador 2. Este post trata sobre un curioso bug que nos reportaron: un crash que dependía de la cantidad de código escrita.

En esta materia los estudiantes aprenden a programar en bajo nivel, comienzan haciendo un trabajo práctico en assembler (x86-64) y terminan programando un mini sistema operativo de 32 bits desde cero, pasando a modo protegido, con interrupciones, memoria virtual y conmutación de tareas.

Un estudiante nos contactó con un problema bastante raro: cuando se ejecutaba el código de inicialización de la Tabla de Descriptores de Interrupción (IDT), crasheaba el sistema. Algo interesante era que este error dependía del la posición en memoria del código. Es decir, si agregaba código en su kernel, o usaba otro compilador, el crash dejaba de ocurrir.

El código de incicialización de la IDT era válido, pero al poner breakpoints antes de su ejecución notamos que el mismo era modificado. Poniendo un watchpoint sobre una de las direcciones de memoria modificadas, nos topamos con que el watchpoint indicaba que la siguiente instrucción a ejecutar era la primer instrucción luego de la etiqueta start, que es el código que se encarga de pasar de modo real a modo protegido.

El código de inicio del TP que les proveíamos a los estudiantes era de la siguiente forma:

;; Saltear seccion de datos
jmp start

;; Seccion de datos.
start_rm_msg db     'Iniciando kernel en Modo Real'
start_rm_len equ    $ - start_rm_msg

start_pm_msg db     'Iniciando kernel en Modo Protegido'
start_pm_len equ    $ - start_pm_msg

;; Seccion de codigo.

;; Punto de entrada del kernel.
BITS 16
start:
    ; Deshabilitar interrupciones
    cli

    ; Cambiar modo de video a 80 X 50
    mov ax, 0003h
    int 10h ; set mode 03h
    xor bx, bx
    mov ax, 1112h
    int 10h ; load 8x8 font

    ; Imprimir mensaje de bienvenida
    print_text_rm start_rm_msg, start_rm_len, 0x07, 0, 0

Comenzaba con una instrucción de jmp que saltaba por encima de un bloque de memoria usado para definir algunos mensajes para imprimir en pantalla, ejecutando luego el código para pasar de modo real a modo protegido (en 16 bits). Este mismo template del trabajo práctico, se usó por más de 6 años, sin ningún problema reportado hasta el momento.

El estudiante en cuestión había modificado ese código agregando más mensajes para imprimir por pantalla, y eso fue lo que disparó el problema. En particular, el último string que agrego fue su número de libreta universitaria, que terminaba en "/15".

El error estaba en que la primer instrucción (jmp start), es una instrucción que se ejecuta en modo real (16 bits), pero nada está diciéndole al ensamblador que tiene que ensamblarla para 16 bits, como sí ocurre con el BITS 16 antes de la etiqueta start. Por lo cual, esta instrucción se ensambla como una instrucción de 32 bits.

¿Pero cómo es que esto funcionaba hasta ahora entonces? Bueno, hay varios tipos de jmp. Un «jump short» es cuando el desplazamiento es un entero de 8-bits con signo, y se codifica como 0xeb xx donde xx es el desplazamiento. Este era el caso para el template que les dabamos para el trabajo práctico: como los mensajes definidos ocupaban poco espacio, el ensamblador emitía un «jump short», y eso se codificaba igual en 16 y 32 bits.

Al agregar más texto entre el jmp start y la etiqueta start, el ensamblador no pudo emitir más un «jump short» y tuvo que emitir «jump near, relative»: un salto relativo, cuyo desplazamiento puede ser de 16 o 32 bits, según el tamaño de operación, que en modo real es 16 bits, y en modo protegido es 32 bits normalmente.

Esta instrucción se codifica como 0xe9 xx xx para 16 bits o como 0xe9 xx xx xx xx para 32 bits. El desplazamiento es relativo al eip al final de la instrucción.

En el caso del trabajo práctico, el offset era 0x97, así que la instrucción emitida era e9 97 00 00 00, pero al momento de ejecutarse, al estar en modo real, el procesador leía e9 97 00, calculando un desplazamiento de 0x97 relativo al final de esa instrucción (dos bytes antes de donde realmente debería terminar). Esto causaba que el salto se realice a dos bytes antes que start.

Recordemos que el último mensaje que el estudiante había agregado era su libreta universitaria, que terminaba con /15, estos últimos dos caracteres, en hexadecimal corresponden con 31 35, que si lo interpretamos como una instrucción de modo real es: xor word ptr ds:[di], si. Es decir, el procesador ejecutaba "15" como si fuera una instrucción, y eso terminaba haciendo xor de valores en memoria apuntados por registros. En ese punto de la ejecución, el contenido de ds era 0, di era 0x1b00 y si era 0x10e1. Esto caía, de casualidad, en el código de inicialización de la IDT.

Cambiando versiones de compilador o corriendo un poco el código, el error dejaba crashear el sistema ya que terminaba modificando constantes que iban a parar a la IDT para interrupciones que nunca se ejecutaban.

El código del template del trabajo práctico estuvo allí desde el 2014, pero como nadie modificaba la sección de datos entre el jmp start y la etiqueta de start, el bug pasó desapercibido por 6 años.

© Marco Vanotti 2024

Powered by Hugo & new.css.