░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░ ░▒▓ ▓▒░ ░▒▓ Librería de IPX para Quick Basic 4.5 ▓▒░ ░▒▓ |ncubux 1.996/97 ▓▒░ ░▒▓ ▓▒░ ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░
Sonnet's driving and holding the pen for this one, so buckle up XDDDDD. What you've got here is an IPX library in 8086 assembly, written in 1996/97 to send and receive network packets from QuickBasic 4.5. The author was about 17 at the time, had two PCs wired together over coax, and way too much free time. Distributed on BBS boards. It worked. The license at the end is genuinely the best part.
01 — IPX: networking before TCP/IP
Before TCP/IP ate everything, local area networks ran on other protocols. The dominant one in DOS and Windows 3.x environments was IPX (Internetwork Packet Exchange), developed by Novell for its NetWare system. If you had a network at home or in a small business in the 90s, odds are you were running IPX.
IPX is a connectionless network protocol, like UDP: you send a packet, you trust it arrives, there's no handshake or acknowledgement. Every node on the network has a 10-byte address: 4 bytes of network number and 6 bytes of node number (usually the card's MAC address). Packets can be up to 64 KB. To reach every node at once you use the broadcast address: FF:FF:FF:FF:FF:FF.
The IPX driver on DOS loaded at boot — either as part of Novell NetWare's drivers (LSL.COM + card driver + IPXODI.COM), or as the built-in IPX that shipped with Windows 95 and Windows 3.11 with Workgroups. Once loaded, it exposed its interface through interrupt 7Ah.
02 — The problem: Basic can't reach the driver
QuickBasic 4.5 is a high-level language running in DOS real mode. It has access to files, screen and keyboard, but no way to talk directly to an arbitrary software interrupt or build the memory structures IPX needs.
To call INT 7Ah you need to load specific values into processor registers and pass pointers to memory structures in a specific format. Plain Basic can't do that. The fix: write the critical functions in assembly, compile them as a library, and declare them in Basic as external functions.
QB 4.5 supports two kinds of external library. The QLB loads into the IDE with QB /L ipx_qlb.qlb and lets you use the functions while developing. The LIB is used when compiling and linking the final executable. Same code, packaged two different ways.
03 — The fix: a QLB written in ASM
The full source file, IPX_QLB.ASM, assembled with Turbo Assembler 3.2. The model directive is what lets Basic and ASM understand each other: .MODEL SMALL,BASIC tells the assembler that parameters are passed by reference on the stack in reverse order, following QuickBasic's calling convention. Every procedure is far because Basic calls external code with 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 — INT 7Ah and the ECB
Interrupt 7Ah is the gateway into the IPX driver. The value of BX when you call it determines the operation:
BX=0— Open socketBX=1— Close socketBX=3— Send packetBX=4— Listen for incoming packetBX=9— Get local addressBX=0Ah— Yield CPU to the driver
To send and receive, IPX doesn't work with simple pointers. It uses a structure called the ECB (Event Control Block): a 42-byte block that describes the packet, where it's coming from, where it's going, where it sits in memory, and what state it's in. The driver reads the ECB, does its job, and when it's done sets the in-use field to 0 to signal that the operation completed. An IPX header of 30 bytes always travels alongside the ECB. The user's data sits right after that.
The xchg dh, dl that shows up in several functions exists because IPX stores socket numbers in big-endian (most significant byte first), but x86 is little-endian. You have to swap the bytes every time you pass a number to the driver or read one back from it.
05 — The library's functions
From Basic they're all declared with DECLARE at the top of the program. inIPX uses INT 2Fh (the DOS multiplex interrupt) to detect whether the driver is loaded before trying anything — if it returns 255 (&HFF), IPX is there; if not, go home. turboIPX is a polite call to the driver to yield CPU, the equivalent of a modern yield. intercambia is a plain xchg ah, al packaged as a function so you didn't have to do it in Basic.
06 — The test program
IPX.BAS is a minimal LAN chat: open socket, prepare ECBs, send/receive loop. For the destination it uses full broadcast — it reaches every node without needing to know anyone's address:
'----------------------------------------------------------------------
' 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 — The license
The INFO.TXT inside the ZIP had the usage instructions and the license. I'm reproducing it almost verbatim because it's the best part of the whole project:
Do whatever you want with the library. If you sell it, good for you; if you strip out the copyright... well, that seems a bit dumb to me, but hey, go ahead. If you modify it... isn't that exactly why I included the source?! And one more thing... if it blows up, it wasn't "me" ;DDDDDDDDD
De facto public domain with a liability waiver in 90s emoticons. Hard to improve on.
The library was distributed on BBS boards inside an ARJ archive (IPX_QLB.ARJ) — the trendy compression format before ZIP pushed it out entirely. Inside were the .ASM, the .QLB, the .LIB and the sample .BAS. Everything you needed to start sending IPX packets from Basic in ten minutes — provided you already had the network set up and the driver loaded, which was the actually hard part.