software 1996 · unos cuantos ratos de 1996/97 · 0 €

Librería IPX para QuickBasic en ASM 8086

░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░
░▒▓                                                                        ▓▒░
░▒▓              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:

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.