Allá por el 96 se me ocurrió intentar decodificar las imágenes estáticas del Canal Plus en ensamblador. La idea era lista. La ejecución, más o menos. El resultado, gloriosamente cutre. Aquí lo cuento como lo recordaría el chaval que lo hizo — que era yo, aunque firmaba como |ncubuX por aquello de mantener cierto misterio en los grupos de la BBS.
01 — Qué hacía el Canal Plus con la imagen
El Canal Plus de los 90 usaba un sistema llamado Nagravision (o Syster, según a quién le preguntes) para cifrar la señal de vídeo analógica. No cifraba el audio — ese iba en claro, que si no los descodificadores oficiales habrían costado el doble y la gente se habría cabreado más todavía.
Lo que hacía con la imagen era básicamente dos cosas:
1. Cortar cada línea horizontal en un punto aleatorio y pegar el trozo de la derecha a la izquierda (line cut). 2. Reordenar las líneas de la imagen en un orden pseudoaleatorio que dependía de la clave del frame.
La clave cambiaba varias veces por segundo. Para descodificar "bien" necesitabas la clave, que solo tenía el descodificador oficial (que la recibía cifrada en la línea 16 del teletexto, de memoria). Yo no tenía la clave. Nadie en la BBS la tenía. Pero había una cosa que sí teníamos: física.
02 — La idea (brillante) y el problema (gordo)
En una imagen natural — una película, un partido de fútbol, lo que sea — las líneas horizontales adyacentes se parecen mucho entre sí. La línea 100 y la línea 101 son casi iguales porque el mundo no cambia de golpe de un pixel al siguiente en vertical. Esto se llama correlación espacial y es la base de casi toda la compresión de vídeo.
Así que la idea fue: si tengo las líneas desordenadas, puedo intentar reordenarlas buscando, para cada posición, qué línea del montón tiene menos diferencia con la anterior. La línea que "encaje mejor" es probablemente la que tocaba ahí.
Para cada línea de la imagen (de arriba a abajo):
Para cada línea candidata en el buffer:
error = suma de |pixel_actual - pixel_candidato|
La candidata con menor error => esa va aquí
Intercambiar
Elegante. El problema era uno solo pero gordo: no sabes cuál es la primera línea. La clave del Nagravision determinaba el orden de las líneas Y el punto de corte de cada una, y eso iba variando frame a frame. Así que incluso si mi algoritmo ordenaba las líneas perfectamente a partir de la primera... si la primera era la equivocada, la imagen salía igualmente hecha una pena.
La solución que se me ocurrió fue la de andar probando: la constante s0 en el código era el desplazamiento inicial — qué línea tomaba como "punto de partida". Ponía un número (entre 0 y 399), compilaba, ejecutaba, miraba si la imagen tenía mejor o peor pinta, y cambiaba a mano. Ingeniería de las de toda la vida.
03 — El algoritmo del mínimo error
El núcleo del programa en pseudocódigo decente:
; Intercambia la línea s0 con la primera como punto de arranque
; (apuesta inicial sobre cuál es la "primera" línea real)
para cada línea_actual (de la primera a la última):
error_min = 0xFFFFFFFF
mejor_candidata = ninguna
para cada línea_candidata en el buffer:
error = 0
para cada pixel en la línea (640 pixels):
error += abs(pixel_actual[i] - pixel_candidata[i])
si error < error_min:
error_min = error
mejor_candidata = línea_candidata
intercambiar(línea_actual, mejor_candidata)
En la práctica, el bucle de cálculo de error lo hace en 32 bits (resta bytes pero acumula en DX), y usa el timestamp counter del 486 (RDTSC, el opcode 0Fh 31h que ni existía oficialmente todavía en los manuales que yo tenía) para medir cuánto tardaba. Eso era el "profiler" que menciona la constante stamp del principio.
04 — El código (con DOS32 y todo)
El programa usa DOS32 como extensor de modo protegido. Esto es porque una imagen de 640x480 ocupa 307.200 bytes, que no caben en los 640KB del modo real del DOS. Con DOS32 se podía pedir memoria extendida vía INT 31h (la interfaz DPMI) y trabajar con punteros de 32 bits en modo plano. Nada de segmentación, nada de límites de 64KB. Lujo.
La cabecera del .ASM lo decía todo sobre el autor y sus intenciones:
; ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░ ; Enredando con el PLUS... nada de copyright |ncubuX 96-97 ; ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░ ; Esta fuente emplea DOS32 como extensor de modo protegido... El programa ; este trata de decodificar el plus (como le sale de los huevos)... juega ; con la constante s0 para alcanzar mejores resultados (del 0 al 399). ; No me hago responsable de nada, naturalmente porque aqui no violo absolu- ; tamente nada. Al programa le entra un .raw (yo los hago con el alchemy) ; y escupe un decoded.raw :) ; ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░
"Como le sale de los huevos" era exactamente el nivel de documentación de la época. El flujo del programa:
inicializa:
Modo texto (INT 10h), esconde cursor
Muestra "DECODIFICADOR KUTUSKIANO v1.0 / Setabia's Hell 1.996"
Parsea argumentos (nombre del .raw de entrada)
Abre el fichero fuente
Pide memoria extendida vía DPMI (INT 31h, función EE42h)
Lee la cabecera (size_fichero - 640*480 bytes)
Crea y abre decoded.raw
Escribe la cabecera en el destino
Vuelca los datos al buffer en memoria
vuelve:
Intercambia línea 0 con línea s0 (apuesta inicial)
Para cada línea: busca mínimo error e intercambia
Escribe el buffer a decoded.raw
Cierra ficheros y sale
Y el bucle de búsqueda de error, que es el corazón:
busca_error:
mov al,[esi+ebx] ; pixel de la línea actual
sub al,[edi+ebx] ; menos pixel de la candidata
jns oks ; si positivo, está bien
neg al ; si no, valor absoluto
oks:
add dx,ax ; acumula error
dec ebx
jnz busca_error
Sencillo y directo. Calcula la suma de diferencias absolutas (SAD) entre dos líneas de 640 bytes, acumulando en DX. Si el resultado es menor que el mínimo guardado, esa es la nueva mejor candidata.
05 — Por qué no funcionaba del todo
Funcionaba. Un poco. Más o menos. Depende de la imagen.
El problema fundamental es que el algoritmo greedy de "coge siempre la línea con menor error respecto a la anterior" no garantiza la solución global óptima. Puede que la línea 47 sea la mejor candidata para la posición 5, pero si se la "gastas" en la posición 3 porque ahí también encajaba bastante bien, luego en la 5 te quedas sin ella.
El segundo problema era el que ya mencioné: sin saber cuál es la línea 0, estás jugando a la lotería con s0. En imágenes con mucho movimiento o poca correlación vertical (fondos muy variables) el algoritmo se volvía loco. En imágenes tranquilas — un plano fijo de una película, por ejemplo — a veces salía algo reconocible.
El tercer problema es que el Nagravision también cortaba cada línea horizontalmente por un punto aleatorio, y eso yo no lo trataba. Así que incluso cuando el orden de las líneas era correcto, cada línea estaba partida y pegada al revés. El resultado parecía una imagen vista a través de una persiana puesta por un loco.
¿Servía para ver el fútbol? No. ¿Servía para aprender modo protegido, DPMI, RDTSC y algoritmos de mínimo error? Absolutamente sí. Y para eso se hizo.
06 — El fuente completo (P2011.ASM)
Por si a alguien le apetece leerlo. Sin cambiar ni una coma.
; ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░
; Enredando con el PLUS... nada de copyright |ncubuX 96-97
; ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░
; Esta fuente emplea DOS32 como extensor de modo protegido... El programa
; este trata de decodificar el plus (como le sale de los huevos)... juega
; con la constante s0 para alcanzar mejores resultados (del 0 al 399).
; No me hago responsable de nada, naturalmente porque aqui no violo absolu-
; tamente nada. Al programa le entra un .raw (yo los hago con el alchemy)
; y escupe un decoded.raw :)
; ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░
; includelib debugs.lib
; debugg = 1 ; Poner a 1 si incluyo el debug del DOS32
stamp = 1 ; Poner a 1 para el profiler
s0 EQU 32
xmax EQU 640
ymax EQU 480
cont = 0
CuentaFin MACRO
pushad
db 0Fh, 31h
sub eax,co_low
sbb edx,co_hi
sub eax,oh_low
sbb edx,0
mov baja,eax
popad
ENDM
Contar MACRO
pushad
db 0Fh, 31h
mov ebx,eax
db 0Fh, 31h
sub eax,ebx
mov oh_low,eax
db 0Fh, 31h
mov co_low,eax
mov co_hi,edx
popad
ENDM
;if debugg
; extrn Debug : near
; endif
.386P
.MODEL FLAT
.STACK 1024
.CODE
caa DB 8 DUP(0)
DB "$"
oh_low DD ? ; TIMESTAMP
co_low DD ?
co_hi DD ?
baja DD 0
inicio DB ">>> DECODIFICADOR KUTUSKIANO v1.0 <<<"
DB 13,10," Setabia's Hell 1.996$"
letras DB 13,10,"Error: debes indicar el archivo a ser"
DB " procesado",10,13,"$"
buffer_ptr DD ? ; puntero de cabecera
Fichero_SI DW ? ; manip del archivo fuente
Fichero_DI DW ? ; manip del archivo destino
Error_min DD 0FFFFFFFFh ; error minimo encontrado
Error_off DD ? ; offset de la minerrorline
size_fichero DD ? ; tamanyo del fichero en bytes
size_arg DB ? ; longitud de los argumentos.
print_fich DB "Fichero: $" ; Mensaje
print_memo DB "Memoria requerida: $"
argumentos DB 0FFh DUP(0) ; Linea de comando
crr DB 10,13,"$" ; Retorno de carro
dos_error DB "- Error del DOS -$"
sin_memoria DB 13,10,"No hay memoria libre",13,10,"$"
size_cab DD ? ; tamanyo de la cabecera
fic_sal DB "decoded.raw",0
comienzo:
; if debugg
; call Debug
; endif
jmp inicializa
vuelve:
call cr
; ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░
mov esi,buffer_ptr ; se coge la primera linea
mov edi,xmax*s0
add edi,buffer_ptr
xor ebx,ebx
er34:
mov al,[esi+ebx]
xchg al,[edi+ebx]
mov [esi+ebx],al
inc ebx
cmp ebx,xmax
jna er34
mov esi,buffer_ptr
pilla:
mov edi,esi
add edi,xmax
error_linea:
mov ebx,xmax-4
xor edx,edx
xor eax,eax
; if stamp
contar
; endif
busca_error: ; Bucle de busqueda de errores
mov al,[esi+ebx]
sub al,[edi+ebx]
jns oks
neg al
oks:
add dx,ax
dec ebx
jnz busca_error
; if stamp
cuentafin
; endif
cmp edx,dword ptr error_min
ja sigue_buscando
mov dword ptr error_off,edi ; Linea con maximo error
mov dword ptr error_min,edx
sigue_buscando:
mov edx,buffer_ptr
add edx,xmax*ymax
cmp edi,edx
jae otra_linea
add edi,xmax
jmp error_linea
otra_linea:
add esi,xmax
mov edi,dword ptr error_off
xor ebx,ebx ; Intercambio de lineas
cambia_linea:
mov eax,dword ptr [esi+ebx]
xchg eax,dword ptr [edi+ebx]
mov dword ptr ds:[esi+ebx],eax
add bx,4
cmp bx,xmax
jne cambia_linea
mov dword ptr error_min,0FFFFFFFFh ;(-1) Anyadir desplazamiento
mov edx,xmax*ymax ; de la siguiente linea
add edx,buffer_ptr
cmp esi,edx
ja final1
jmp pilla ; Aun quedan lineas
; ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░
final1:
mov ah,40h ; Salva los datos a "decoded.raw"
mov bx,fichero_DI
mov ecx,size_fichero
sub ecx,size_cab
mov edx,buffer_ptr
int 21h
jc errores
mov ax,3Eh ; Cierra el fichero destino
mov bx,word ptr fichero_DI
jc errores
final_close:
mov ax,3Eh ; Cierra el fichero origen
mov bx,word ptr fichero_SI
jc errores
final:
call Pon_cursor ; Restaura el tamanyo del cursor
mov eax,baja
mov ah,4Ch ; Salida al DOS
int 21h ; y libera la memoria usada
; ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░
; ALLOCATE: meter en size_fichero el tamanyo que quieras coger
; EDX: puntero donde esta el bloque
; ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░
allocate PROC
pushad
mov ax,0EE42h
mov edx,dword ptr size_fichero
int 31h
sub eax,dword ptr size_fichero
jz allocate_ok
xor edx,edx
stc
jmp fin_allocate
allocate_ok:
clc
fin_allocate:
popad
ret
allocate ENDP
; ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░
Quita_cursor PROC ; Esconde el cursor
pushad
mov ch,32
xor cl,cl
mov ah,1
int 10h
popad
ret
Quita_cursor ENDP
; ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░
Pon_cursor PROC ; Restaura el cursor
pushad
mov ch,6
mov cl,7
mov ah,1
int 10h
popad
ret
Pon_cursor ENDP
; ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░
CR PROC ; Imprime un retorno de carro
push edx
push ax
mov ah,9
mov edx,offset crr
int 21h
pop ax
pop edx
ret
CR ENDP
; ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░
; ERRORHANDLER
; ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░
info: ; Como se usa el programa
mov ah,9
mov edx,offset letras
int 21h
jmp final
errores: ; Los errores del DOS
mov ah,9
mov edx,offset dos_error
int 21h
jmp final
no_mem:
mov ah,9
mov edx,offset sin_memoria
int 21h
jmp final_close
; ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░
VAL32 PROC
mov cl,8
xor bl,bl
xor ch,ch
cro:
rol eax,4
mov dl,al
and dl,00001111b
cmp bl,0
jne siguiendo
cmp dl,0
setne bl
siguiendo:
cmp bl,1
jne loo
cmp dl,9
ja letra
add dl,48
jmp printer
letra:
add dl,55
printer:
push ax
mov ah,2
int 21h
pop ax
loo:
loop cro
mov dl,"h"
mov ah,2
int 21h
ret
VAL32 ENDP
; ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░
inicializa:
mov ax,3 ; Modo texto
int 10h
push ds ; Inicializa selectores
pop es
call Quita_cursor ; Hace invisible el cursor
call cr ; Muestra informacion general
mov ah,9
mov edx,offset inicio
int 21h
call cr
mov ax,0EE02h ; Pilla los argumentos necesarios
int 31h
xor ebx,ebx
add esi,80h
mov bl,[esi]
add esi,2
push esi
add esi,ebx
dec esi
mov al,0
mov [esi],al
add esi,1
mov al,"$"
mov [esi],al
pop esi
push esi
call cr
mov ah,9
mov edx,offset print_fich
int 21h
mov edx,esi
int 21h
call cr
pop esi
mov ax,3D02h ; Abre el archivo
xor cx,cx
mov edx,esi
int 21h
jc errores
mov word ptr fichero_SI,ax
mov ax,4202h ; Coge el tamanyo del fichero fuente
mov edx,0
mov bx,word ptr fichero_SI
int 21h
jc errores
mov dword ptr size_fichero,eax
mov edx,eax
call allocate ; Pide memoria de acuerdo con el fich.
mov ax,0EE42h
int 31h
mov dword ptr buffer_ptr,edx
cmp eax,0
jz no_mem
push eax
mov ah,9
mov edx,offset print_memo
int 21h
pop eax
; call val32
mov edx,size_fichero ; Tamanyo de la cabecera
sub edx,xmax*ymax
mov size_cab,edx
mov ax,4200h ; Restaura al principio
mov edx,0
mov bx,word ptr fichero_SI
int 21h
jc errores
mov ah,3Fh ; vuelca la cabecera hacia el buffer
mov bx,fichero_SI
mov ecx,size_cab
mov edx,dword ptr buffer_ptr
int 21h
jc errores
mov ah,3Ch ; Crea el fichero de salida (y abre)
mov cx,0
mov edx,offset fic_sal
int 21h
jc errores
mov ax,fichero_DI
mov ax,3D02h ; Abre el archivo
xor cx,cx
mov edx,offset fic_sal
int 21h
jc errores
mov word ptr fichero_DI,ax
mov ah,40h ; Salva la cabecera
mov bx,fichero_DI
mov ecx,size_cab
mov edx,buffer_ptr
int 21h
jc errores
mov ah,3Fh ; vuelca los datos al buffer
mov bx,fichero_SI
mov ecx,size_fichero
sub ecx,size_cab
mov edx,dword ptr buffer_ptr
int 21h
jc errores
jmp vuelve
END comienzo
— |ncubuX / Setabia's Hell, 1.996. Nada de copyright, naturalmente.