░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░ ░▒▓ ▓▒░ ░▒▓ Librería de IPX para Quick Basic 4.5 ▓▒░ ░▒▓ |ncubux 1.996/97 ▓▒░ ░▒▓ ▓▒░ ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░
Sonnet al volante y la pluma en este artículo, agarrense que vienen curvas XDDDDD. Lo que tenéis aquí es una librería de IPX en ensamblador 8086 escrita en 1996/97 para poder mandar y recibir paquetes de red desde QuickBasic 4.5. El autor tenía entonces unos 17 años, dos PCs conectados por coaxial y demasiado tiempo libre. Distribuida en BBS. Funcionaba. La licencia al final es literalmente lo mejor.
01 — IPX: la red de antes del TCP/IP
Antes de que TCP/IP se comiera todo, las redes de área local usaban otros protocolos. El dominante en entornos con PCs con DOS y Windows 3.x era IPX (Internetwork Packet Exchange), desarrollado por Novell para su sistema NetWare. Si en los 90 tenías una red en casa o en una empresa pequeña, casi con toda seguridad corrías IPX.
IPX es un protocolo de red sin conexión, como UDP: mandas un paquete, confías en que llegue, no hay handshake ni confirmación. Cada nodo en la red tiene una dirección de 10 bytes: 4 bytes de número de red y 6 bytes de número de nodo (que suele ser la MAC de la tarjeta). Los paquetes pueden ser de hasta 64 KB. Para comunicarte con todos los nodos a la vez usas la dirección de broadcast: FF:FF:FF:FF:FF:FF.
El driver de IPX en DOS se cargaba al arrancar — bien como parte de los drivers de Novell NetWare (LSL.COM + driver de tarjeta + IPXODI.COM), bien como el IPX integrado que llevaban Windows 95 y Windows 3.11 con Workgroups. Una vez cargado, exponía su interfaz a través de la interrupción 7Ah.
02 — El problema: Basic no llega al driver
QuickBasic 4.5 es un lenguaje de alto nivel que corre en modo real de DOS. Tiene acceso a ficheros, pantalla y teclado, pero no tiene ninguna forma de hablar directamente con una interrupción software arbitraria ni de construir las estructuras de datos que necesita IPX.
Para llamar a la INT 7Ah necesitas meter valores concretos en registros del procesador y pasar punteros a estructuras de memoria de formato específico. En Basic puro no se puede. La solución: escribir las funciones críticas en ensamblador, compilarlas como librería, y declararlas en Basic como funciones externas.
QB 4.5 soporta dos tipos de librería externa. La QLB se carga en el IDE con QB /L ipx_qlb.qlb y permite usar las funciones mientras desarrollas. La LIB se usa al compilar y linkar el ejecutable final. Son el mismo código empaquetado de dos maneras distintas.
03 — La solución: una QLB hecha en ASM
El fichero fuente completo, IPX_QLB.ASM, ensamblado con Turbo Assembler 3.2. La directiva de modelo es la clave para que Basic y ASM se entiendan: .MODEL SMALL,BASIC indica que los parámetros se pasan por referencia en la pila en orden inverso, siguiendo la convención de llamada de QuickBasic. Cada procedimiento es far porque Basic llama a código externo con CALL FAR.
;════════════════════════════════════════════════════════════════════════════
; Librería de IPX para Quick Basic 4.5 |ncubux 1.996/97
;════════════════════════════════════════════════════════════════════════════
.MODEL SMALL,BASIC
.CODE
Public abrir
;═══════════════════════════════════════════════════════════════════
; ABRIR: Abre un socket
;═══════════════════════════════════════════════════════════════════
; Entradas: entero => 00: para cerrar todo al terminar
; 0FFh: para uso en TSR
; entero => socket a ser abierto (0 -> dinamico)
; Salidas : un entero (en AX)
; 00: éxito
; 0FEh: Tabla de sockets llena
; 0FFh: El socket está en uso
; Declaración:
; DECLARE FUNCTION abrir%(tipo%,socket%)
; Uso:
; tipo% = 0: socket% = &H7777
; errorlevel% = abrir% (tipo%, socket%)
;═══════════════════════════════════════════════════════════════════
abrir PROC far,tipo:word, socket:word
mov bx,tipo
mov ax,[bx]
mov bx,socket
mov dx,[bx]
xor bx,bx
xchg dh,dl ; IPX usa big-endian; x86 es little-endian
int 7Ah
xor ah,ah
xor dx,dx
ret
abrir EndP
Public cerrar
;═══════════════════════════════════════════════════════════════════
; CERRAR: Cierra un socket
;═══════════════════════════════════════════════════════════════════
; Entradas: primer entero: socket a ser cerrado
;═══════════════════════════════════════════════════════════════════
cerrar PROC far, socket:word
mov bx,socket
mov dx,[bx]
xchg dh,dl
mov bx,1
int 7Ah
ret
cerrar EndP
Public midir
;═══════════════════════════════════════════════════════════════════
; MIDIR: coge tu dirección de red en formato 4:6 (bytes)
;═══════════════════════════════════════════════════════════════════
; Entradas: dirección de la variable con tipo dlocal
; Salidas : rellena la variable con tu dirección local
; Declaración: DECLARE SUB midir(direc AS INTEGER)
; Uso:
; TYPE dlocal
; red AS STRING * 4
; nodo AS STRING * 6
; END TYPE
; DIM midireccion AS dlocal
; CALL midir(VARPTR(midireccion))
;═══════════════════════════════════════════════════════════════════
midir PROC far, offs:word
mov bx,offs
mov si,[bx] ; SI = offset de la variable a rellenar
mov ax,ds
mov es,ax ; QB 4.5 solo usa memoria cercana
mov bx,9 ; EN ES:SI => dirección a rellenar
int 7Ah
ret
midir EndP
Public mandar
;═══════════════════════════════════════════════════════════════════
; MANDAR: manda un paquete, descrito con un ECB de salida
;═══════════════════════════════════════════════════════════════════
; Entradas: la dirección del ECB de salida
; Declaración: DECLARE SUB Mandar (ECBOffset AS INTEGER)
; Uso:
; DIM ECBM AS ECB
; CALL mandar(VARPTR(ECBM))
;═══════════════════════════════════════════════════════════════════
mandar PROC far,desplazamiento:word
mov bx,desplazamiento
mov si,[bx]
mov ax,ds
mov es,ax
mov bx,3
int 7Ah
ret
mandar EndP
Public recibir
;═══════════════════════════════════════════════════════════════════
; RECIBIR: recibe un paquete por un ECB de entrada
;═══════════════════════════════════════════════════════════════════
; Entradas: la dirección del ECB de entrada
; Salidas : un errorlevel% que si es distinto de 0 da error
; Declaración: DECLARE FUNCTION recibir% (ECBOffset AS INTEGER)
; Uso:
; DIM ECBR AS ECB
; errorlevel% = recibir(VARPTR(ECBR))
;═══════════════════════════════════════════════════════════════════
recibir PROC far,desplazamiento:word
push es
mov bx,desplazamiento
mov si,[bx]
mov ax,ds
mov es,ax
mov bx,4
xor ax,ax
xor dx,dx
int 7Ah
xor ah,ah
pop es
ret
recibir EndP
Public turboIPX
;═══════════════════════════════════════════════════════════════════
; TURBOIPX: le dice al driver IPX que tu programa no
; necesita mucha velocidad, para que asi coja
; él más recursos de la CPU
; Declaración:
; DECLARE SUB turboIPX()
;═══════════════════════════════════════════════════════════════════
turboIPX PROC far
mov bx,0Ah
int 7Ah
ret
turboIPX EndP
Public inIPX
;═══════════════════════════════════════════════════════════════════
; inIPX: mira a ver si está el driver de IPX cargado
; Salidas: un byte que si es <> de 255 NO está cargado
; Declaración:
; DECLARE FUNCTION inIPX%()
;═══════════════════════════════════════════════════════════════════
inIPX PROC far
mov ax,7A00h
int 2Fh ; INT 2Fh = interrupción multiplex de DOS
xor ah,ah
ret
inIPX EndP
Public intercambia
;═══════════════════════════════════════════════════════════════════
; intercambia: intercambia los bytes de un entero (big/little endian)
; Entrada: un entero
; Salida : el entero cambiado
; Declaración:
; DECLARE FUNCTION intercambia%(numero AS INTEGER)
;═══════════════════════════════════════════════════════════════════
intercambia PROC far,numero:word
xor dx,dx
mov bx,numero
mov ax,[bx]
xchg ah,al
ret
intercambia EndP
Public PMandar
;═══════════════════════════════════════════════════════════════════
; PMandar: prepara un paquete para ser mandado
; Entradas:
; socket => el socket que se va a usar
; tama => tamaño del paquete de datos (sin cabecera)
; ECB => dirección del ECB de salida
; IPX => dirección de la cabecera IPX de salida
; nodo => dirección del nodo destino (6 bytes)
; paquete=> dirección de los datos a mandar
;═══════════════════════════════════════════════════════════════════
PMandar PROC far,socket:word,tama:word,ECB:word,IPX:word,nodo:word,paquete:word
push es
mov ax,ds
mov es,ax
mov bx,ecb ; Borra el ECB (42 bytes a cero)
mov di,[bx]
xor ax,ax
mov cx,42
rep stosb
mov bx,IPX ; Borra la cabecera IPX (30 bytes)
mov di,[bx]
mov cx,30
xor ax,ax
rep stosb
mov bx,socket ; Transfiere el socket al ECB
mov dx,[bx]
xchg dh,dl
mov bx,ECB
mov di,[bx]
mov es:[di+0Ah],dx
mov ax,1 ; Número de fragmentos = 1
mov es:[di+22h],ax
mov bx,IPX ; Offset de la cabecera IPX
mov ax,[bx]
mov es:[di+24h],ax
mov ax,es ; Segmento
mov es:[di+26h],ax
mov bx,tama ; Tamaño total = datos + 30 bytes cabecera
mov ax,[bx]
add ax,30
mov es:[di+28h],ax
add di,1Ch ; Dirección inmediata (nodo destino en ECB)
mov bx,nodo
mov si,[bx]
mov ax,[si]
mov [di],ax
mov ax,[si+2]
mov [di+2],ax
mov ax,[si+4]
mov [di+4],ax
mov bx,IPX ; Checksum IPX = FFFFh (sin checksum)
mov di,[bx]
mov ax,0FFFFh
mov [di],ax
push di
mov bx,9 ; Rellenar dirección fuente con la local
mov si,12h
add si,di
int 7Ah
pop di
mov bx,socket ; Socket fuente y destino
mov dx,[bx]
xchg dh,dl
mov [di+1Ch],dx
mov [di+10h],dx
push di
mov bx,9 ; Red de destino = nuestra red
mov si,6
add si,di
int 7Ah
pop di
mov bx,tama ; Tamaño en cabecera IPX (big-endian)
mov ax,[bx]
add ax,30
xchg al,ah
mov [di+2],ax
mov bx,IPX ; Nodo de destino en cabecera IPX
mov di,[bx]
add di,10
mov bx,nodo
mov si,[bx]
mov ax,[si]
mov [di],ax
mov ax,[si+2]
mov [di+2],ax
mov ax,[si+4]
mov [di+4],ax
pop es
ret
PMandar EndP
Public PRecibir
;═══════════════════════════════════════════════════════════════════
; PRecibir: prepara un ECB de recogida
; Entradas:
; socket => el socket que se va a usar
; tama => tamaño del paquete de datos
; ECB => dirección del ECB de entrada
; IPX => dirección de la cabecera IPX de entrada
;═══════════════════════════════════════════════════════════════════
PRecibir PROC far, ECB:word, IPX:word, tama:word, socket:word
mov ax,ds
mov es,ax
mov bx,ecb ; Borra el ECB
mov di,[bx]
xor ax,ax
mov cx,42
rep stosb
mov bx,IPX ; Borra la cabecera IPX
mov di,[bx]
mov cx,30
xor ax,ax
rep stosb
mov bx,socket ; Socket al ECB
mov dx,[bx]
xchg dh,dl
mov bx,ECB
mov di,[bx]
mov es:[di+0Ah],dx
mov ax,1 ; Un fragmento
mov es:[di+22h],ax
mov bx,IPX ; Offset cabecera IPX
mov ax,[bx]
mov es:[di+24h],ax
mov ax,es ; Segmento
mov es:[di+26h],ax
mov bx,tama ; Tamaño = datos + 30
mov ax,[bx]
add ax,30
mov es:[di+28h],ax
mov al,1Dh ; Marcar ECB como "en uso"
mov [di+8],al
mov bx,ECB ; Llamar a recibir (BX=4)
mov si,[bx]
mov bx,4
xor ax,ax
xor dx,dx
int 7Ah
xor ah,ah
ret
PRecibir EndP
END
04 — La INT 7Ah y el ECB
La interrupción 7Ah es la puerta de entrada al driver IPX. El valor de BX al llamarla determina la operación:
BX=0— Abrir socketBX=1— Cerrar socketBX=3— Enviar paqueteBX=4— Preparar recepciónBX=9— Obtener dirección localBX=0Ah— Ceder CPU al driver
Para enviar y recibir, IPX no trabaja con punteros simples. Usa una estructura llamada ECB (Event Control Block): un bloque de 42 bytes que describe el paquete, su origen, su destino, dónde está en memoria y en qué estado se encuentra. El driver lee el ECB, hace su trabajo, y cuando termina pone el campo uso a 0 para indicar que la operación completó. Junto al ECB va siempre una cabecera IPX de 30 bytes. Los datos del usuario van pegados a continuación.
El xchg dh, dl que aparece en varias funciones existe porque IPX almacena los números de socket en big-endian (el byte más significativo primero), pero el x86 es little-endian. Hay que intercambiar los bytes cada vez que se pasa un número al driver o se recibe uno de él.
05 — Las funciones de la librería
Desde Basic se declaran todas con DECLARE al principio del programa. inIPX usa la INT 2Fh (interrupción multiplex de DOS) para detectar si el driver está cargado antes de intentar nada — si devuelve 255 (&HFF), hay IPX; si no, a casa. turboIPX es una llamada cortés al driver para cederle CPU, equivalente a un yield moderno. intercambia es puro xchg ah, al empaquetado como función para no tener que hacerlo en Basic.
06 — El programa de prueba
El IPX.BAS es un chat de LAN mínimo: abre socket, prepara ECBs, bucle de envío y recepción. Para el destino usa broadcast total — llega a todos los nodos sin necesitar saber la dirección de nadie:
'----------------------------------------------------------------------
' Pruebas con el IPX usando la IPX_QLB
'------------------------------------------------------ FUNCIONES IPX
DECLARE FUNCTION abrir% (tipo AS INTEGER, socket AS INTEGER)
DECLARE SUB cerrar (socket AS INTEGER)
DECLARE SUB midir (offset AS INTEGER)
DECLARE FUNCTION inIPX% ()
DECLARE SUB turboIPX ()
DECLARE SUB Mandar (ECBM AS INTEGER)
DECLARE FUNCTION recibir% (ECBS AS INTEGER)
DECLARE FUNCTION intercambia% (numero AS INTEGER)
DECLARE SUB pmandar (socket%, tama%, v%, u%, dest%, da%)
DECLARE SUB precibir (ECB%, IPX%, tama%, socket%)
'---------------------------------------------------------------------- TIPOS
TYPE mides
m1 AS INTEGER
m2 AS INTEGER
m3 AS INTEGER
END TYPE
TYPE dired
red AS STRING * 4
nodo AS STRING * 4
no1 AS STRING * 2
socket AS INTEGER
END TYPE
TYPE dlocal
red AS STRING * 4
nodo AS STRING * 4
no1 AS STRING * 2
END TYPE
TYPE ECB ' Cabecera de transporte IPX
link AS LONG
ESR AS LONG
uso AS STRING * 1 ' 0 = operación completada
completo AS STRING * 1
socket AS INTEGER
work AS STRING * 16
inmediata1 AS STRING * 6
cuenta AS INTEGER
datosoff AS INTEGER
datoseg AS INTEGER
tama AS INTEGER
END TYPE
TYPE CabeceraIPX
checksum AS INTEGER
longitud AS INTEGER
control AS STRING * 1
paquete AS STRING * 1
destino AS dired
fuente AS dired
paq AS STRING * 1000 ' datos de usuario
END TYPE
'---------------------------------------------------------------------- PPAL
CLS
DIM ecbr AS ECB ' ECB de llegada (para recibir)
DIM CR AS CabeceraIPX ' Cabecera para recibir
DIM destino AS mides ' Nodo de destino
DIM socket AS INTEGER
DIM tipon AS INTEGER
DIM ECBM AS ECB ' ECB de salida (para mandar)
DIM CM AS CabeceraIPX ' Cabecera para mandar
'--- Localizando al driver de IPX
errorlevel = inIPX
IF errorlevel <> 255 THEN
PRINT "- Controlador de IPX no cargado -"
END
ELSE
PRINT "> El controlador de IPX está presente"
END IF
'--- Abriendo un socket
tipon = 0
socket = &H869C
CALL cerrar(socket) ' por si estaba abierto de antes
errorlevel = abrir(tipon, socket)
SELECT CASE errorlevel
CASE 0 : PRINT "> Socket abierto: "; HEX$(socket); "h"
CASE &HFE : PRINT "- Tabla de sockets llena -"
CASE &HFF : PRINT "- El socket ya estaba abierto -"
CASE ELSE : PRINT "- Error no definido -"
END SELECT
'--- Preparando paquete de salida
tama% = LEN(CM.paq)
v% = VARPTR(ECBM)
u% = VARPTR(CM)
dest% = VARPTR(destino)
da% = VARPTR(CM.paq)
CALL pmandar(socket, tama%, v%, u%, dest%, da%)
'--- Preparando paquete de entrada
tama% = LEN(CR.paq)
v% = VARPTR(ecbr)
u% = VARPTR(CR)
CALL precibir(v%, u%, tama%, socket)
x% = recibir(VARPTR(ecbr))
CLS
'--- Bucle principal: mandar y recibir
DO
IF ASC(ecbr.uso) = 0 THEN ' uso=0 => paquete recibido
COLOR 14
PRINT "Recibido: "; CR.paq
x% = recibir(VARPTR(ecbr)) ' reengancharse para el siguiente
END IF
q$ = INKEY$
IF q$ <> "" THEN
INPUT "=>", linea$
IF linea$ = "quita" THEN GOTO kkk
COLOR 15
CM.paq = linea$
destino.m1 = (&HFFFF) ' FF:FF:FF:FF:FF:FF = broadcast
destino.m2 = (&HFFFF)
destino.m3 = (&HFFFF)
tama% = LEN(CM.paq)
v% = VARPTR(ECBM)
u% = VARPTR(CM)
dest% = VARPTR(destino)
da% = VARPTR(CM.paq)
CALL pmandar(socket, tama%, v%, u%, dest%, da%)
DO
LOOP UNTIL VAL(ECBM.uso) = 0 ' esperar confirmación de envío
CALL Mandar(VARPTR(ECBM))
END IF
LOOP
kkk:
CALL cerrar(socket)
PRINT "> Socket cerrado: "; HEX$(socket); "h"
07 — La licencia
El INFO.TXT del ZIP incluía las instrucciones de uso y la licencia. La reproduzco casi literal porque es lo mejor de todo el proyecto:
Haz lo que quieras con la librería. Si la vendes, pues eso que te ganas; si le sacas el copyright... pues hombre, un poco estupido me parece pero vamos. Si la modificas... ¿¿para eso he metido la fuente?! Y otra cosa... si explota yo no he "sio" ;DDDDDDDDD
Dominio público de facto con exención de responsabilidad en emojis de los 90. Difícilmente mejorable.
La librería se distribuyó en BBS en un ARJ (IPX_QLB.ARJ) — el formato de compresión de moda antes de que ZIP lo desbancara del todo. Dentro iban el .ASM, el .QLB, el .LIB y el .BAS de ejemplo. Todo lo necesario para ponerte a mandar paquetes IPX desde Basic en diez minutos, si tenías la red ya configurada y el driver cargado, que eso era lo difícil de verdad.