software 1996 · a handful of evenings in 1996/97 · 0 €

IPX library for QuickBasic in 8086 assembly (1996)

🌐 Leer esta página en español →
░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░
░▒▓                                                                        ▓▒░
░▒▓              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:

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.