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.