Site hosted by Angelfire.com: Build your free website today!

ASSEMBLER 80x86 CAPITULO 2

  • DECIMO MENSAJE El Registro de Estados (Primera Parte) Flags Z y C
  • DECIMOPRIMER MENSAJE El Registro de Estados (Segunda Parte) Flag C (continuación) Flags P, A, S, T, I, O y D
  • DECIMOSEGUNDO MENSAJE Las instrucciones de strings (Primera Parte) CMPS y LODS
  • DECIMOTERCER MENSAJE Las instrucciones de strings (Segunda Parte) MOVS, SCAS, STOS
  • DECIMOCUARTO MENSAJE Estructura de un programa (Primera Parte) Los programas .COM
  • DECIMOQUINTO MENSAJE Estructura de un programa (Segunda Parte) Cómo terminar un programa .COM
  • DECIMOSEXTO MENSAJE Estructura de un programa (Tercera Parte) Parámetros en la línea de comandos
  • DECIMOSEPTIMO MENSAJE Estructura de un programa (Cuarta Parte) Los programas .EXE
  • DECIMOCTAVO MENSAJE Indicie General de la Serie
  • APENDICE A Notación Hexadecimal: Un punto de vista computacional
  • APENDICE B Unos pocos tricks en assembler
    INDICE GENERAL DE LA SERIE

    Lo primero que hay que saber (décima parte)

    El registro de Estados
    Ya vimos que el 8088 tiene los siguientes registros:
    AX, BX, CX, DX      Generales: c/u se puede dividir en 2 de 1 byte
    SI, DI              Generales: ppalmente usados para manejo de strings
    BP                  General: Usado ppalmente para acceder al stack
    SP                  Usado por la CPU para acceder al Stack
    DS, ES, CS, SS      Contienen el número de párrafo de los segmentos
    IP                  Puntero de Instrucción. Usado por la CPU para
                        dirigirse al código ejecutable.
    
    Dentro del micro existe otro registro que no tiene nombre simbólico y que se llama *Registro de Estados* o de *Flags*.

    Este registro contiene información en sus bits. Aunque no todos sus bits se utilizan.

    La idea es que cada bit de este registro es un flag con dos estados posibles: 1 y 0. Ciertas instrucciones del lenguaje máquina ponen a 1 o a 0 uno o más de esos flags y ciertas otras efectúan tareas condiciondas al estado de algunos de esos flags.

    El programador no necesita recordar qué flag corresponde a qué bit, ya que los diferentes flags tienen nombre simbólico y uno se refiere a ellos por su nombre.

    Veamos una descripción detallada de los flags con ejemplos de su uso:

    ;--------;
    ; FLAG Z ;
    ;--------;
    Flag de Zero. Se pone a 1 cuando el resultado de la última operación fue
    "0" o cuando el resultado de la última comparación fue "igual". En
    cambio Flag Z=0 cuando el resultado de la última operación fue diferente
    de "0" o cuando el resutado de la última comparación fue "distinto".
    
    Ejemplos:
      SUB AX,BX             ; Operación de resta
      JZ DIO_CERO           ; Jump if Zero, salta si Flag Z=1
      JNZ NO_DIO_CERO       ; Jump if Not Zero, salta si Flag Z=0
    
      CMP AX,BX             ; Compara AX con BX
      JE SON_IGUALES        ; Jump if Equal, salta si Flag Z=1
      JNE SON_DISTINTOS     ; Jump if Not Equal, salta si Flag Z=0
    
      TEST AX,BX            ; Hace un test bit a bit de AX con BX
      JZ DISJUNTOS          ; Si ningún bit coincide, Flag Z=1
      JNZ BIT_EN_COMUN      ; Si hay algún bit en común, Flag Z=0
    
    Conviene notar que JZ y JE son dos nemónicos de la misma instrucción de código máquina. Uno elige JZ o JE según el contexto para brindar mayor claridad. Por ejemplo después de una CoMParación conviene escribir JE en lugar de JZ. Lo mismo vale para JNZ y JNE.
    ;--------;
    ; FLAG C ;
    ;--------;
    Flag de Carry. Se pone a 1 cuando en la última operación hubo un
    acarreo (algo parecido al "me llevo 1" cuando se efectúa una suma).
    También sirve para saber si el resultado de una comparación fue "menor"
    o "mayor o igual". Si Flag C=1, el primer operando de la comparación era
    MENOR que el segundo, si Flag C=0, el primer operando de la comparación
    era MAYOR O IGUAL que el segundo.
    
    Ejemplos:
    VALOR_1   DW 5678H      ; Word menos significativa de 12345678H
              DW 1234H      ; Word más significativa
    RESULTADO DW ?          ; Word menos significativa o LO
              DW ?          ; Word más significativa o HI
    
    SUMA:
    ;------------------------------------;
    ; Calcua RESULTADO = BX:AX + VALOR_1 ;
    ; BX:AX contiene un número de 4 bytes;
    ; AX = 2 bytes menos signicativos    ;
    ; BX = 2 bytes más significativos    ;
    ;------------------------------------;
      CLC                   ; Clear Carry. Fuerza Flag C=0
      ADC AX,VALOR_1        ; AX = AX + LO(VALOR_1) + Flag C. Si se lleva 1
                            ; lo pone en el Carry Flag.
      MOV RESULTADO,AX      ; Salvar word menos significativa
      ADC BX,VALOR_1+2      ; BX = BX + HI(VALOR_1) + Flag C. Si se lleva 1
                            ; lo pone en el Carry Flag.
      MOV RESULTADO+2,BX    ; Salvar word más significativa.
      JC OVERFLOW           ; Si Flag C=1, resultado no entra en 4 bytes
      ...                   ; Si Flag C=0, resultado entró en 4 bytes
                            
    La instrucción ADC op1,op2 suma así: op1 = op1 + op2 + Flag C.
    La instrucción ADD op1,op2 suma así: op1 = op1 + op2.
    Ambas instrucciones dejan Flag C=1 si el resultado de la suma excede la
    longitud de los operandos (que pueden ser de 1 byte o 2 bytes).
    
      CMP AX,BX             ; Compara AX con BX
      JB MENOR              ; Jump if Below, salta si AX es menor que BX
      JAE MAYOR O IGUAL     ; Jump if Above, Salta si es mayor o igual
    
    JB y JC son sinónimos. Ambas saltan si Flag C=1 indicando que en la comparación el primer operando era menor que el segundo. JAE y JNC también son sinónimos. Saltan si Flag C=0, indicando que el resultado de la comparación fue mayor o igual.

    El Flag C también se usa para saber si el resutado de una rutina fue o exitoso o no.

    Ejemplo:
    VER_SI_MAYUSCULA: CMP AL,'A' ; Compara AL con el ASCII de "A" JB > L2 ; Si es menor salir con Flag C=1 indicando error CMP AL,'Z'+1 ; Compara AL con el ASCII de "Z" + 1 JAE > L2 ; Si es mayor o igual, error L1: CLC ; Clear Carry indicando salida por OK RET L2: STC ; SeT Carry indicando salida por error RET Luego CALL VER_SI_MAYUSCULA ; LLamar a la rutina de arriba JC NO_MAYUSCULA ; Si vuelve Flag C=1, no era mayúscula JNC MAYUSCULA ; Si C=0, AL contenía una letra mayúscula Una versión mejorada: VER_SI_MAYUSCULA: CMP AL,'A' ; Compara AL con "A" JB > L1 ; Si Below, es porque C=1. Salir. CMP AL,'Z'+1 ; Compara AL con "Z"+1 CMC ; CompleMent Carry: invierte Flag C L1: RET
    Resumen parcial:
    No hay una manera directa de acceder al Flag Z.
    
    Para cambiar el Flag C existen las instrucciones:
    
    CLC       Pone Flag C=0
    STC       Pone Flag C=1
    CMC       Invierte Flag C
    
    Hay otros flags y los vamos a ver en el próximo mensaje.

    Lo primero que hay que saber (decimoprimera parte)

    El Registro de Estados (segunda parte)
     
    ;-----------------------;
    ; Flag C (continuación) ;
    ;-----------------------;
    En el mensaje anterior vimos que uno de los usos de Flag C era para
    sumar números de más de 15 bits. Bien, también sirve para restar ese
    tipo de cantidades. Veamos un ejemplo:
    
    VALOR_1   DW 5678H      ; Word menos significativa de 12345678H
              DW 1234H      ; Word más significativa
    RESULTADO DW ?          ; Word menos significativa o LO
              DW ?          ; Word más significativa o HI
    
    RESTA:
    ;------------------------------------;
    ; Calcua RESULTADO = BX:AX - VALOR_1 ;
    ; BX:AX contiene un número de 4 bytes;
    ; AX = 2 bytes menos signicativos    ;
    ; BX = 2 bytes más significativos    ;
    ;------------------------------------;
      CLC                   ; CLear Carry. Fuerza Flag C=0.
      SBB AX,VALOR_1        ; AX = AX - LO(VALOR_1) - Flag C.
      MOV RESULTADO,AX      ; Salvar word menos significativa. Si pidió 1
                            ; pone Flag C=1.
      SBB BX,VALOR_1+2      ; BX = BX - HI(VALOR_1) - Flag C. Si pidió 1
                            ; pone Flag C=1.
      MOV RESULTADO+2,BX    ; Salvar word más significativa.
      JC NEGATIVO           ; Si C=1, se restó algo grande de algo chico
      ...                   ; Si C=0, resultado entró en 4 bytes
    
    La instrucción SBB op1,op2 resta así: op1 = op1 - op2 - Flag C. La instrucción SUB op1,op2 resta así: op1 = op1 - op2. Ambas instrucciones dejan Flag C=1 si op1 era menor que op2 (pidió 1).

    Aquí conviene tener en cuenta la coherencia que existe entre las instrucciones CMP y SUB con respecto al manejo del Flag C. SUB op1,op2 resta op2 de op1 y pone Flag C=1 si pidió 1. Esto ocurre cuando op1 --------Quienes tengan experiencia con el 6510 (COMMODORE 64 o 128), notarán que el manejo del Flag C es inverso al del 8088. En el 6510 las instrucciones de comparación entre op1 y op2 ponen el Flag C a cero cuando op1 ;--------; ; Flag P ; ;--------; Flag de Paridad. Se pone a 1 cuando el resultado de una operación es par y a 0 en caso contrario. Se lo usa en combinación con las instrucciones JP (Jump if Parity flag) o JNP (Jump if Not Parity flag).

    ;--------; ; Flag A ; ;--------; Flag Auxiliar. Actúa como un Flag C pero a nivel de la mitad de los operandos. Por ejemplo si al restar un byte de otro el nibble bajo le pidió 1 al nibble alto, entonces Flag A=1. Del mismo modo, si al sumar dos words, hubo un acarreo del byte bajo al byte alto, Flag A=1. Lo usan las instrucciones de aritmética decimal. No hay instrucciones de salto asociadas a este Flag.

    ;--------; ; Flag S ; ;--------; Flag de Signo. Se pone a 1 cuando el resultado es negativo. Recordemos que el micro usa la técnica llamada "complemento a 2". Por eso los valores negativos son los que tienen el bit más alto en 1. Para operandos de 1 byte este es el bit 7, para operandos de 2 bytes este es el bit 15. Ejemplos de representaciones binarias de números negativos. Valor Byte Word -1 FF FFFF -2 FE FFFE -3 FD FFFD

    Para convertir un valor negativo de un byte en el mismo valor pero ocupando 2 bytes existe la instrucción CBW (Convert Byte to Word) que hace lo siguiente: Si AL < 80H, pone AH = 0 Si AL >=80H, pone AH = FF

    Ejemplos:
    CBW AL ---> AX 1 0001 80 FF80 El Flag S puede usarse en combinación con las instrucciones de salto JS y JNS.

    Asociado a este flag tenemos otras instrucciones de salto condicional JG, JL y sus derivados. Como estas instrucciones dependen del valor de tres flags: Z, S y O vamos a explicarlas después de ver el Flag O más abajo.

    ;--------;
    ; Flag T ;
    ;--------;
    Flag de Trap. Afecta el funcionamiento del micro de la siguiente forma:
    cuando Flag T = 1, cada vez que se termina de ejecutar una instrucción
    del lenguaje máquina se produce una interrupción. La interrupción que se
    produce es la INT 3. Este es el flag que utilizan los debuggers para
    permitir la ejecución paso a paso de un programa. La instrucción INT
    (entre otras cosas que hace) pone Flag T=0.
    
    ;--------;
    ; Flag I ;
    ;--------;
    Flag de Interrupciones. Cuando Flag I=0 el micro no atiende los pedidos
    de interrupción del hardware y solamente atiende las no enmascarables.
    Las interrupciones del software se atienden aunque Flag I=0. Este flag
    se lo maneja directamente con las instrucciones
    
     CLI      ; CLear Interrupt que lo pone a 0
     STI      ; SeT Interrupt que lo pone a 1
    
    La instrucción INT también pone Flag I=0, impidiendo las interrupciones.
    Si bien existen formas de deshabilitar selectivamente interrupciones del
    hardware, normalmente los programas usan CLI y STI para habilitarlas o
    desabilitarlas en forma conjunta.
    
    ;--------;
    ; Flag O ;
    ;--------;
    Flag de Overflow. Se pone a 1 cuando en el resultado se produjo un
    Overflow. Esto significa que el bit más alto se vio afectado por un
    acarreo proveniente de los bits más bajos. Se usa en las operaciones con
    signo (recordar que el signo es el bit más alto) para saber si después
    de la operación el bit de signo es correcto o incorrecto.
    
    Por ejemplo si AX = 4501H, entonces ADD AX,4B00H da como resultado AX =
    8001H. Pero atención: 4501H y 4B00H son ambos positivos y sin embargo
    80001 es negativo!
    
    Lo que pasa es que si estamos trabajando con enteros con signo de 2
    bytes los valores deben entrar en 15 bits ya que el decimosexto bit (bit
    15) está reservado para el signo. Esto debe ser así tanto para los
    operandos como para los resultados. Dado que FFFFH = 65535, los enteros
    con signo de dos bytes deben estar comprendidos entre -32768 y 32767.
    
    Además de existir las instrucciones de salto condicional JO y JNO,
    exsite una instrucción especial INTO que produce una INT 4 si Flag O=1.
    
    Ahora estamos en condiciones de ver las instrucciones de salto
    condicional JL y JG:
    
      JG    ; Jump if Greater: salta si Flag Z=0 y además Flag S = Flag O
            ; O sea, el resultado no fue cero y además:
            ;       el signo es positivo (S=0) y válido (O=0)
            ; o bien
            ;       el signo es negativo (S=1) pero inválido (O=1)
    
      JL    ; Jump if Less: salta si Flag Z=0 y además Flag S <> Flag O
            ; O sea, el resultado no fue cero y además:
            ;       el signo es negativo (S=1) y válido (O=0)
            ; o bien
            ;       el signo es positivo (S=0) pero inválido (O=1)
    
    Las instrucciones asociadas a JG y JL se deducen fácilmente de estas
    descripciones.
    
    ;--------;
    ; Flag D ;
    ;--------;
    Flag de Dirección. Este flag está ligado a un conjunto de instrucciones
    para manejo de strings de bytes o words. Debido a la importancia de ese
    conjunto, vamos a dedicar un mensaje completo a describir el Flag D y a
    esas instrucciones.
    
    
    El registro de Flags:
    F E D C B A 9 8 7 6 5 4 3 2 1 0 +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ | | | | | O | D | I | T | S | Z | | A | | P | | C | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ Los bits sin nombre no se usan en el 8088 (aunque algunos sí se usan en otros miembros de la familia 80x86) Otras instrucciones para manejo del Registro de Flags: PUSHF ; Apila el Registro de Flags POPF ; Desapila el Registro de Flags LAHF ; Copia Flags S Z A P C a bits 7 6 4 2 0 de AH SAHF ; Copia bits 7 6 4 2 0 de AH a Flags S Z A P C Digamos finalmente que la mayoría de las instrucciones alteran uno o más Flags del Registro de Estados. La lista es demasiado larga para recordarla. Un programador necesita tener a mano una cartilla con las instrucciones del micro en donde poder consultar una descripción del funcionamiento de las instrucciones y en especial la forama en que estas actúan sobre los Flags. La Norton Guides brindan una buena asistencia en este sentido.

    Lo primero que hay que saber (décimosegunda parte)

    Las instrucciones de strings
    El 8088 tiene una importante cantidad de instrucciones dedicadas al tratamiento de cadenas de bytes o words (el 80386 tiene -además- instrucciones para el manejo de cadenas de bits!). Ellas nos facilitan las tareas con cadenas de datos: buscar un elemento, leerlas, compararlas, copiarlas, etc. En estas instrucciones se usan especialmente los registros SI y DI como índices a las cadenas a tratar: DS:SI -> "fuente" ES:DI -> "destino" Si bien cada instrucción de este tipo actúa sobre un solo byte o palabra, la ventaja de usarlas está en que incrementan automaticamente a los registros índices y que pueden actuar sobre una cadena entera si se los acompa¤a con el prefijo REP (y sus derivados). También cobra especial importancia el Flag D (de Dirección), con el que controlamos el sentido en que se recorrerá el string. Todas las instrucciones que vamos a ver tienen dos versiones: agregando al nemónico B o W trabajarán a nivel de bytes o de words. 1) CMPS (CoMPare String) PASS_LEN EQU 6 ; Longitud de la password PASSWORD DB 'TSOHCP' ; Por ejemplo "PCHOST" al revés TIPEADO DB PASS_LEN DUP ? ; Poner aquí la password ingresada CHECK_PASS: ;---------------------------------------------------; ; Compara string de longitud PASS_LEN con una clave ; ; ; ; INPUT: ; ; PASWORD = String Clave ; ; TIPEADO = String a comparar ; ; OUTPUT: ; ; Si Flag Z=1, String=Clave ; ; Si Flag Z=0, String<>Clave ; ;---------------------------------------------------; MOV SI,OFFSET TIPEADO ; DS:SI -> Clave ingresada MOV DI,OFFSET PASSWORD ; ES:DI -> Clave verdadera MOV CX,PASS_LEN ; CX = cantidad de bytes a comparar CLD ; INC SI y DI después de c/compararación REP CMPSB ; Comparar un byte tras otro y DEC CX RET ; Si sale con Flag Z=1, eran iguales
    Observaciones:
    a) En el ejemplo estamos suponiendo que DS y ES tienen los valores correctos. b) CLD es una instrucción que pone a cero el Flag D (Dirección) del Registro de Estados. Cuando Flag D=0, las instrucciones de string incrementan los registros índices (SI y DI). Cuando Flag D=1, los decrementan (recorrido inverso). c) El prefijo REP hace todo esto: 1. Ejecutar CMPSB 2. Si los bytes comparados son diferentes, salir del REP con Flag Z=0 3. Decrementar contador CX 4. Si CX<>0, volver a 1 5. Si CX=0, los strings son iguales. Salir con Flag Z=1. d) CMPSB después de comparar incrementa SI y DI. Por lo tanto, si los strings son diferentes, se sale del REP con Flag Z=0 SI y DI apuntando al byte siguiente al que acaba de ser comparado Por eso, decrementando SI y DI, quedaríamos apuntando al primer byte donde los strings difieren. e) También existe la versión CMPSW que compara las words apuntadas por DS:SI y ES:DI. En este caso SI y DI se incrementan o decrementan en 2 (según el valor del Flag D). 2) LODS (LOaD String) NUM_STR DB '1635',0 ; StringZ con un valor numérico ASCII_TO_BIN: ;---------------------------------------------; ; Convierte StringZ numérico en valor binario ; ; ; ; INPUT: ; ; NUM_STR = StringZ numérico sin signo ; ; OUTPUT: ; ; BX = Valor binario del strigz ; ;---------------------------------------------; MOV SI,OFFSET NUM_STR ; DS:SI -> StringZ numérico CLD ; Recorrer strings hacia adelante XOR BX,BX ; Guardar el resultado en BX XOR AH,AH ; AH=0 a lo largo de toda la rutina LODSB ; Leer el primer byte en AL CMP AL,0 ; Ver si el stringz era vacío JE RET ; Si vacío, salir con BX=0 L1: AND AL,0FH ; Ya que '0'=30H, '1'=31H,..., '9'=39H SHL BX,1 ; Multiplicar BX por 2 MOV DX,BX ; Guardar en DX SHL BX,1 ; por 4 SHL BX,1 ; por 8 ADD BX,DX ; BX fue multiplicado por 10 ADD BX,AX ; Agregar dígito recién leído LODSB ; AL=nuevo dígito CMP AL,0 ; Ver si fin de stringz JNE L1 ; No, continuar RET
    Observaciones:
    a) Se supone que DS apunta al segmento de datos donde está NUM_STR. b) En este caso no se necesita usar ES:DI ya que nos referimos solamente a un string. c) Suponemos que el string tiene un valor que entra en dos bytes (inferior a 65.536). d) La instrucción SHL BX,1 produce un corrimiento (Shift) hacia la izquierda de los bits de BX. Esto equivale a multiplicar BX por 2. Si 2*BX es mayor que 65.535, entonces Flag C se pondrá a 1. Consultando este Flag después de cada SHL se podría controlar un eventual overflow. En ese caso habría que hacer lo mismo después de cada ADD. e) Obsérvar que la multiplicación por 10 de BX se hace a través de SHL y ADD y se evita el uso de la instrucción MUL, por ser esta más lenta e incómoda. f) También existe LODSW que carga AX con la word apuntada por DS:SI e incrementa (o decrementa) SI en 2. g) No tiene sentido usar LODS en combinación con REP (ni ninguno de sus derivados). Después de leído un byte o word uno necesita hacer algo con él antes de leer el siguiente.

    Lo primero que hay que saber (décimotercera parte)

    Las instrucciones de strings (segunda y última parte)
    Continuamos describiendo las instrucciones de tratamiento de strings de bytes y words del 8088. 3) MOVS (MOVe String) VIDEO_SEG EQU 0B800H ; 0B000H para la Hercules X_WIN DB ? ; Coordenada X (izquierda) Y_WIN DB ? ; Coordenada Y (arriba) W_WIN DB ? ; Ancho (Width) de la ventana H_WIN DB ? ; Altura de la ventana BUFFER DW 80*25 DUP ? ; Para guradar la ventana SAVE_WIN: ;--------------------------------------------------------; ; Salva en memoria una ventana de pantalla con atributos ; ; ; ; INPUT: ; ; X_WIN, Y_WIN, W_WIN, H_WIN ; ;--------------------------------------------------------; MOV AX,VIDEO_SEG ; ES = VIDEO_SEG MOV ES,AX ; Hacer el MOV a través de AX MOV DI,OFFSET BUFFER ; DI -> Buffer en memoria MOV AL,Y_WIN ; AL = # de filas arriba de la ventana MUL 80 ; Cada fila tiene 80 caracteres ADD AX,X_WIN ; Sumar X_WIN SHL AX ; AX=2*AX, cada caracter ocupa 2 bytes MOV SI,AX ; SI -> comienzo de ventana en VideoRam MOV BL,H_WIN ; Cantidad de renglones MOV DX,80 ; Ancho de pantalla (en bytes) SUB DL,BL ; DX = # de bytes que hay entre el fin SHL DL,1 ; de un renglón y el principio del sgte XOR CH,CH ; CH = 0 MOV AL,W_WIN ; AL = ancho de la ventana XCHG DS,ES ; ES:DI -> BUFFER, DS:SI -> Video RAM CLD ; Recorrer strings hacia adelante L1: MOV CL,AL ; CX = cantidad de words a mover REP MOVSW ; Copiar CX words ADD SI,DX ; Pasar al renglón siguiente DEC BL ; Un renglón menos JNE L1 ; Repetir para todos los renglones XCHG DS,ES ; Reparar DS RET
    Observaciones:
    a) Suponemos que DS apunta al segmento de datos. b) XCHG DS,ES es una macro del A86. Equivale a PUSH DS, PUSH ES seguidos de POP DS, POP ES. c) Originalmente se pone en ES el segmento de la fuente y no del destino. Después se intercambian DS con ES. Eso se hace despés de haber hecho referencia a las cuatro variables X_WIN, Y_WIN, W_WIN, H_WIN. De otro modo DS no estaría apuntando a nuestro segmento de datos. d) Dentro del bucle que comienza en L1 se evita el uso de variables de memoria ya que DS apunta al segmento de video y no de datos. e) Antes de salir de la rutina se vuelve DS a la normalidad. No conviene que una rutina destruya el valor de un registro de segmento. f) Hay que tener presente que cada caracter de pantalla ocupa 2 bytes. En el primero está el ASCII del caracter, en el segundo su color (o atributo) g) Por supuesto, también existe la versión MOVSB que mueve de a un byte. 4) SCAS (SCAn String) CMD_LIN EQU 80H ; Segunda parte del PSP IDENTIF EQU '/' ; Identificador de parámetros FIND_IDENTIF: ;------------------------------------------------------------; ; Busca el identificador '/' en la línea de comandos del DOS ; ; ; ; OUTPUT: ; ; Si Flag Z = 1, el caracter '/' se encontró ; ; en la línea de comandos. En ese caso ; ; DI -> caracter siguiente a '/' ; ; Si Flag Z = 0, no se encontró el caracter '/' ; ;------------------------------------------------------------; MOV CX,[CMD_BUF] ; Longitud de línea de parámetros MOV DI,CMD_BUF+1 ; ES:DI -> parámetros MOV AL,IDENTIF ; AL = caracter a comparar CLD ; Recorrido hacia adelante REPNE SCASB ; Buscar AL dentro del string RET ; Si sale Flag Z=1, se encontró ; Si sale Flag Z=0, no se encontró
    Observaciones:
    a) DOS usa los 128 (80H) segundos bytes del PSP como buffer de la línea de comandos. Si uno llama a un programa desde el prompt del DOS y a continuación del nombre del programa escribe algo, DOS deja ese algo a partir del byte con offset 81H del PSP (no incluye el nombre del programa). El string termina con 0DH (Carriage Return). En el offset 80H, DOS deja la cantidad de caracteres del string escrito por el operador. Esa longitud no incluye al byte 0DH puesto por DOS. b) Los programas usan esta facilidad que brinda el DOS para tomar desde el prompt ciertos parámetros dejados por el operador. Normalmente esos parámetros se deben ingresar antecedidos por algún caracter especial, como '/'. c) El prefijo REPNE es una variante de REP. Significa "repetir mientras no sea igual". Produce la repetición de la instrucción SCASB un máximo de CX veces y hasta que el byte en AL sea encontrado. d) SCASB está asociado con ES:DI. En este ejemplo hemos supuesto que ES apunta al segmento del PSP. e) Al salir de REPNE, ES:DI apunta al byte siguiente al último examinado. Por lo tanto, si el identificador fue encontrado, ES:DI sale apuntando al byte siguiente a '/'. f) Cuando se usa la versión SCASW, lo que se hace es comparar el contenido de AX con el de la word de memoria apuntada por ES:DI. Eso significa que AL se comaparará con el byte en ES:DI y AH con el byte en ES:DI+1. 5) STOS (STOre String) Esta instrucción hace el trabajo inverso al que realiza LODS. Aquí vamos a aprovechar un ejemplo de Hector Ricciardolo. El programa sirve para llenar la pantalla con un caracter y un color determinados. VIDEO_SEG EQU 0B800H ; 0B000H para Hércules CARACTER EQU '±' ; Caracter para llenar la pantalla ATTRIBUTO EQU 23 ; Color. En Hércules 0FH es brillante. FILAS EQU 25 COLUMNAS EQU 80 SCR_FILL: MOV AX,VIDEO_SEG ; ES -> Segmento de Video RAM MOV ES,AX ; No existe MOV DS,VIDEO_SEG MOV DI,0 ; ES:DI -> Video RAM MOV AL,CARACTER ; el byte bajo contiene el ASCII MOV AH,ATTRIBUTO ; el bajo, el color o (atributo) MOV CX,FILAS*COLUMNAS ; Cantidad de caracteres de la pantalla CLD ; Avanzar REP STOSW ; Copiar a toda la pantalla RET

    Lo primero que hay que saber (décimocuarta parte)

    Estructura de un programa.
    Bajo DOS tenemos dos tipos de archivos ejecutables: los progarmas con terminación .EXE y los programas con terminación .COM (también existen archivos ejecutables con extensión .BAT pero esos pertenecen a otra categoría puesto que son archivos de texto que llaman a comandos del DOS o a programas de aplicación los cuales son del tipo .EXE o del tipo .COM) Cuando uno invoca un programa entrando su nombre en el prompt del DOS, éste lo carga y le asigna toda memoria disponible desde el primer párrafo libre hasta el último (recordemos que un párrafo es una dirección absoluta que es múltiplo de 10H). Después de esto, el control es transferido al programa. Este proceso de carga es diferente en el caso de los .EXE que en el de los .COM. No se puede saber de antemano cuál va a ser ese primer párrafo libre. Las direcciones de 0:0 a 0:3FF son ocupadas por los vectores de interrupción, las direcciones siguientes (40:0 a 50:0) las usa la BIOS para guardar sus variables y a continuación vienen los Device Drivers, el COMMAND.COM y otros programas residentes. Después de todo esto comienza la memoria libre que como se vé depende del tama¤o del COMMAND.COM (versión del DOS) y de los progrmas residentes que puede haber en ese momento.
    Los programas .COM
    Una de las características de los .COM es que los cuatro registros de segmento CS, DS, ES y SS son inicializados al mismo valor (el del primer párrafo libre) en el momento en que DOS efectúa la carga en memoria. De ese modo todas las variables y las rutinas tienen una dirección que está determinada por su offset dentro del segmento asignado. Como consecuencia el programa queda confinado a 64 Kbytes (1 segmento) de memoria. Y esa memoria debe ser compartida por el código, las variables, y la pila. Sabemos que la pila crece hacia abajo; es decir, cada vez que se apila una word, el valor de SP decrece en 2. Eso significa que las posiciones más altas del segmento deberían estar libres de código y datos. De otro modo se corre el riesgo de que la pila sobreescriba a otras partes del programa destruyendo datos o código. El código del programa debe comenzar obligatoriamente en la dirección hexadecimal 100H (=256). Veamos por qué: Cuando DOS carga un programa en memoria (.EXE o .COM) destina los primeros 100H bytes a una tabla llamada PSP (Program Segment Prefix) con información que no viene al caso describir ahora. Los bytes ocupados por el PSP son los que tienen offset 00H a FFH dentro del primer segmento asignado al programa. En el caso de un .COM el DOS le transfiere el control al 1er byte que sigue al PSP, es decir al que se encuentra en el offset 100H. Por lo tanto justo en el offset 100H comienza el programa tal cual lo ha escrito el programador en assembler. Por este motivo, un programa .COM debe comenzar con código ejecutable y no con varibles. De otro modo el control se transferiría a una zona de datos, el micro trataría de interpretar esos bytes de datos como si fueran código ejecutable y se producirían resultados impredecibles. Notar también que los 100H bytes del PSP no forman parte del archivo ejecutable en disco (sea este .COM o .EXE). El programador no debe preocuparse por el PSP, en él DOS le ha dejado datos que pueden servirle. 0 100H FFFFH memoria --+-------+----+------------+---------- - - - -------+-memoria baja | PSP |JMP | varibles | código stack| alta ------+-------+--|-+------------+|--------- - - - -------+----- comienzo | | fin del del segmento +------>--------+ segmento saltar sobre las ^ variables | +--<--- Aquí comenzaba el primer párrafo libre antes que DOS cargara este programa. Por este motivo los programas .COM comienzan con un JMP a la rutina principal y entre el JMP y dicha rutina se ubican las variables. Las variables deben estar definidas antes que las mencione el código del programa. Esto es para que el ensamblador (que no interpreta las instrucciones sino que meramente reemplaza nombres simbólicos por direcciones y códigos de operación) sepa qué tipo de variables representan esos nombres.
    Ejemplo.
    Una buena forma de comprender esto es usando el programa DEBUG. Este programa permite (entre otras cosas) ensamblar directamente código ejecutable en la memoria. C:\A86>DEBUG -A 2F2D:0100 MOV DX,110 2F2D:0103 MOV AH,9 2F2D:0105 INT 21 2F2D:0107 RET 2F2D:0108 -E110 'PC-Host BBS 746-1635 TLD 24Hs',0D,0A,'$' -G=100 PC-Host BBS 746-1635 TLD 24Hs Program terminated normally - El prompt del DEBUG es un signo - (menos). Cuando aparece el prompt ingresamos el comando "A" (Assemble) para indicarle al DEBUG que vamos a ensamblar código. En ese momento DEBUG nos escribe la dirección donde vamos a ensamblar el código. El valor del segmento (2F2D en este caso) indica el primer párrafo libre como expliqué más arriba. El offset 100H es proporcionado automáticamente ya que como vimos el programa debe comenzar 100H bytes a partir del principio del segmento. Escribimos un peque¤o programa que sirve para imprimir un string en pantalla. El valor 110 que movemos a DX indica el offset donde comienza nuestro string. Al escribir la instrucción MOV DX,110 estamos inventando una dirección (que debe estar más alla del código) en la que vamos a poner nuestro string. El DEBUG no permite el uso de nombres simbólicos de modo que tenemos que referirnos a los datos por sus direcciones numéricas. Una vez que escribimos el programa controlamos que este termine antes de la dirección 110H (termina en la 107H) e invocamos el comando "E" que sirve para Editar memoria. El signo "$" indica a la función 9 de la INT 21H el final del string. Los valores hexadecimales 0D y 0A son los ASCII de Carriage Return y Line Feed que llevarán el cursor al principio del renglón siguiente. El comando G=100 sirve para ejecutar nuestro programa. También podemos grabar nuestro programa en disco. Para eso entramos los siguientes comandos: -RCX CX 0000 :0032 -N PRUEBA.COM -W Writing 00032 bytes RCX nos muestra el valor actual del Registro CX (0000) y nos permite modificarlo. Ponemos 0032 para indicar que queremos salvar 32H bytes (suficientes en este caso para contener el código y los datos). N PRUEBA.COM indica el Nombre que queremos dar al archivo. W (Write) graba el archivo en disco. Luego podemos salir del DEBUG con el comando Q (Quit). Una vez ahí podemos ver: C:\A86>DIR PRUEBA.COM Volume in drive C is STACKER Directory of C:\A86 PRUEBA COM 50 07/10/92 23:00 1 file(s) 50 bytes que efectivamente hemos creado un archivo de 32H (=50) bytes. Ahora lo podemos ejecutar como a cualquier .COM: C:\A86>PRUEBA PC-Host BBS 746-1635 TLD 24Hs C:\A86>

    Lo primero que hay que saber (décimoquinta parte)

    Estructura de un programa (continuación)
    En el mensaje anterior vimos que los programas .COM son cargados por DOS con los valores de DS, ES, CS y SS inicializados al mismo párrafo (el primero de la memoria libre contigua). De este modo todos las direcciones de un programa .COM se obtienen como un offset dentro de ese párrafo. Conviene aclarar sin embargo que los programas .COM pueden usar toda la RAM disponible para guardar datos y para el uso de la pila.
    Ejemplo 1:
    Mover la pila fuera del segmento asignado por DOS Un programa .COM puede mover la pila fuera del segmento que le ha sido designado del siguiente modo: MOV AX,CS ; Copiar CS en AX ADD AX,01000H ; Avanzar 64 Kbytes CMP AX,0A000H ; Controlar que queda dentro de los 640Kb JAE NO_MEM ; Si fuera de los 640, dejar el Stack quieto MOV SS,AX ; Poner el Stack Segment en ese segmento ... ATENCION: Si se cambia el Stack Segment, el programa .COM no puede terminar con un simple RET (ver la explicación más abajo).
    Ejemplo 2:
    Ubicar un gran buffer fuera del segmento asignado por DOS BUFF_SEG DW ? ; Salvar aquí el valor del segmento para buffer MOV AX,SS ; Copiar SS en AX ADD AX,01000H ; Avanzar 64 Kbytes CMP AX,0A000H ; Controlar que queda dentro de los 640 Kbytes JAE NO_MEM ; Si fuera de los 640, memoria insuficiente MOV BUFF_SEG,AX ; Salvar nuevo segmento en variable de memoria ... Luego, el programa podría usar ese segmento para poner un buffer de lectura de disco (suponemos que el archivo ya había sido abierto): MOV AH,3FH ; Función para leer CX bytes de archivo abierto MOV BX,HANDLE ; BX = Handle asignado por DOS al abrir el file MOV CX,NRO_BYTES ; CX = Cantidad de bytes que se desean leer MOV DX,BUFF_SEG ; La función 3FH pide DS:DX -> buffer MOV DS,DX ; Inicializamos DS a través de DX MOV DX,0 ; y ponemos en DX el offset dentro del buffer INT 21H ; Leer CX bytes del archivo JC ERROR ; Las funciones del DOS devuelven Flag C = 1 ... ; cuando se produce un error ATENCION: el código anterior deja DS apuntando al segmento donde está nuesto buffer y no al segmento del programa donde están las variables. Para acceder a las variables es necesario restituir el valor de DS: PUSH CS POP DS

    Cómo terminar un programa .COM

    1) Terminar con RET Cuando DOS carga un .COM inicializa la primera word del stack a 0 (o sea, el programa arranca con SP=0FFFEH y SS:[SP]=0000H). Esto significa que si terminamos con el programa con una instrucción RET, entonces este último RET transferirá el control a la dirección con offset 0000H en el CS actual. Esta dirección es el comienzo del PSP. Los dos primeros bytes del PSP contienen el código de la instrucción INT 20H. Esta instrucción termina el programa y transfiere el control al DOS. Por lo tanto si no hemos cambiado el valor de SS (ver ejemplo más arriba) podemos simplemente terminar nuestro .COM con RET. 2) Terminar con INT 20H Otra forma de terminar un programa .COM es con una llamada explícita a la INT 20H. 3) Terminar con la función 4CH de la INT 21H Este último método es el más conveniente. Simplemente hay que escribir el código: MOV AH,04CH INT 21H La ventaja de la función 04CH de la INT 21H es que el valor que tenga AL puede ser recogido por un programa .BAT con la variable ERRORLEVEL. Para indicar que el programa ha terminado sin errores tendríamos que salir con AL=00H. En general la convención es que los errores más severos corresponden a valores más grandes de AL. En los archivos .BAT hay que comparar la variable ERRORLEVEL con los posibles valores de mayor a menor. Por ejemplo, si sabemos que nuestro porgrama en assembler puede arrojar códigos de error iguales a 0, 1 o 2, en un .BAT tendríamos que escribir: @ECHO OFF REM Opcional PRUEBA REM Nombre del programa en assembler If ERRORLEVEL = 2 Goto Error2 REM ErrorLevel más alto If ERRORLEVEL = 1 Goto Error1 REM Repetir bajando en 1 Goto OK REM ErrorLevel 0 :Error2 Pause "El programa terminó con ERRORLEVEL 2" Goto END :Error1 Pause "El programa terminó con ERRORLEVEL 1" Goto END :OK Pause "El programa terminó sin errores" :END 4) Terminar quedando residente Los programas que quedan residentes al finalizar terminan su código con la INT 27H (antigua) o la INT 31H (recomendada). Sin embargo no vamos a entrar ahora en detalles de cómo dejar residente un programa ya que es un tema para analizar en profundidad (ver la serie "Todo sobre las interrupciones").

    Lo primero que hay que saber (décimosexta parte)

    Estructura de un programa (continuación)
    Cuando DOS carga un programa en memoria usa los primeros 100H (=256) bytes para el Program Segment Prefix. Esta tabla contiene información útil tanto para el programa como para el DOS. No es difícil encontrar descripciones detalladas del PSP en la literatura, de modo que nos vamos a concentrar solamente en una parte de éste. La segunda mitad del PSP (offset 80H en adelante) es de gran utilidad para el programador en assembler. Muchos programas admiten parámetros que corresponden a diferentes opciones. Por ejemplo, la mayoría de los procesadores de texto permiten que ingresemos el nombre del documento a editar en la línea de comandos.
    Ejemplo:
    C:\A86>QEDIT prueba.8 El DOS toma el string que escribimos a continuación del nombre del programa que queremos ejecutar y coloca una copia a partir del offset 81H del PSP. De ahí los programas pueden analizar la información tipeada por el operador y actuar en consecuencia. En realidad el DOS no solamente copia lo que escribimos sino que además deja en el byte que está en el offset 80H del PSP la longitud del string y termina con un byte 0DH de Carriage Return. Por ejemplo, si tenemos un programa que se llama PRUEBA, lo podemos llamar del siguiente modo: C:\A86>PRUEBA Escribo lo que quiero hasta 127 bytes Una vez que el programa PRUEBA sea cargado en memoria, en el offset 80H del PSP tendremos la longitud del string: " Escribo lo que quiero hasta 127 bytes"; en el offset 81H comenzará el string y el byte sigiente al último (letra 's' de 'bytes') contendrá el valor 0DH.
    Ejemplo:
    Usemos el DEBUG y el programita PRUEBA.COM para ver esto. C:\A86>DEBUG PRUEBA.COM Hola -D80 389B:0080 05 20 48 6F 6C 61 0D 00-00 00 00 00 00 00 00 00 . Hola.......... 389B:0090 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 389B:00A0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 389B:00B0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 389B:00C0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 389B:00D0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 389B:00E0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 389B:00F0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ -U100,107 389B:0100 BA1001 MOV DX,0110 389B:0103 B409 MOV AH,09 389B:0105 CD21 INT 21 389B:0107 C3 RET -D110,12E 389B:0110 50 43 2D 48 6F 73 74 20-42 42 53 20 20 37 34 36 PC-Host BBS 746 389B:0120 2D 31 36 33 35 20 54 4C-44 20 32 34 48 73 24 -1635 TLD 24Hs$ - El comando -D80 sirve para pedir un volcado de memoria (Dump) a partir del offset 80H. Vemos que el string ' Hola' aparece a partir del offset 81H. En el offset 80H está la longitud del string (05) y en el 86H el ASCII de Carriage Return (0D). Cuando termina el PSP (offset 0FFH), comienza nuestro programa PRUEBA cuyo desensamble pedimos con U100,107. Finalmente con D110,12E vemos un volcado del string que imprime PRUEBA. Esto que podemos ver con el DEBUG ocurre del mismo modo cuando simplemente ingresamos: C:\A86>PRUEBA Hola La palabra ' Hola' aparece a partir del offset 81H del PSP. Cuando solamente escribimos el nombre del programa que queremos ejecutar, en el offset 80H del PSP habrá un 0 y en el 81H un 0D.
    Ejemplo:
    Veamos como ejemplo un programa que convierte un número decimal a su expresión hexadecimal. Vamos a suponer que el número se escribe en la línea de comandos a continuación del nombre del programa, que llamaremos DEC2HEX. ; Ensamblar con el A86 JMP MAIN ; Saltar sobre los datos TABLA_HEXA DB '0123456789ABCDEF' HLP DB 0DH,0AH DB ' DEC2HEX convierte un número decimal a expresión hexadecimal' DB 0DH,0AH,0AH DB ' Sintaxis: DEC2HEX dec_val' DB 0DH,0AH,0AH DB ' donde: "dec_val" es un número decimal < 65536' DB 0DH,0AH,0AH DB '$' USO: MOV DX,OFFSET HLP ; DS:DX -> String a emitir MOV AH,09 ; Función del DOS para imprimir un string$ INT 21H ; LLamar al DOS MOV AX,4C01H ; Salir con ERRORLEVEL 1 INT 21H ; Devolver control a DOS MAIN: CALL SALTAR_ESPACIOS ; Saltar sobre los espacios antes del parámetro JZ USO ; Si no hay parámetro, motrar mensaje de help CALL STRING_TO_BIN ; Transformar parámetro a su valor numérico CALL EMITIR_BX ; Emitir las cifras hexadecimales MOV AX,4C00H ; Función para terminar (ERRORLEVEL 0) INT 21H ; Devolver el control al DOS SALTAR_ESPACIOS: ;----------------------------------------------------------------; ; Saltea todos los espacios que hay entre el nombre del programa ; ; y el parámetro en la línea de comandos. ; ; ; ; OUTPUT: ; ; Si Flag Z = 1, no hay parámetro ; ; Si Flag Z = 0, DS:SI -> primer caracter del parámetro ; ; AL = primer carácter del parámetro ; ; CX = cantidad de bytes remanentes ; ;----------------------------------------------------------------; MOV SI,80H ; SI -> Offset 80H del PSP CLD ; Flag D (Dirección) = 0 LODSB ; AL = Longitud del string en línea de comandos CMP AL,0 ; Ver si hay parámetros JE RET ; Si no hay, salir con Flag Z = 1 XOR CH,CH ; CH = 0 MOV CL,AL ; CX = Longitud del string en línea de comandos MOV AH,' ' ; AL = ASCII de ' ' L1: LODSB ; Leer un caracter en AL CMP AL,AH ; Comparar AL con ' ' JNE RET ; Si no es igual, ya se salteraron los espacios LOOP L1 ; Si es espacio, continuar (hasta CX=0) RET STRING_TO_BIN: ;----------------------------------------------------------------; ; Transforma el string de un número decimal en un valor numérico ; ; ; ; NOTA: Cuando la subrutina es llamada AL ya contiene el primer ; ; caracter del string y DS:SI apunta al segundo ; ; OUTPUT en BX ; ;----------------------------------------------------------------; XOR BX,BX ; Resultado inicialmente nulo XOR AH,AH ; AH = 0 a lo largo de toda la rutina L1: SUB AL,'0' ; Restar ASCII de '0' CMP AL,'9'-'0' ; Ver si AL estaba entre '0' y '9' JA RET ; Si mayor, no estaba entre '0' y '9', salir SHL BX,1 ; BX = (BX anterior) * 2 MOV DX,BX ; Salvar en DX SHL BX,1 ; BX = (BX anterior) * 4 SHL BX,1 ; BX = (BX anterior) * 8 ADD BX,DX ; BX = (BX anterior) * 10 ADD BX,AX ; BX = (BX anterior) * 10 + nueva cifra LODSB ; Leer otra cifra del string LOOP L1 ; Y continuar si no se acabó el string RET EMITIR_BX: ;---------------------------------------------; ; Emite las cuatro cifras hexadecimales de BX ; ;---------------------------------------------; MOV DX,BX ; Salvar BX en DX MOV BX,OFFSET TABLA_HEXA ; DS:BX -> TABLA_HEXA MOV AL,DH ; Primero emitir DH CALL SHOW_AL ; Hacerlo desde AL MOV AL,DL ; Después emitir DL JMP SHOW_AL ; con la misma rutina SHOW_AL: MOV AH,AL ; Salvar AL en AH SHR AL,1 ; Bajar el nibble alto SHR AL,1 ; de AL al nibble bajo SHR AL,1 ; En las 186, 286 y 386 SHR AL,1 ; se puede poner SHR AL,4 XLAT ; AL = cifra hexadecimal correspondiente CALL PRINT_AL ; Emitir la cifra (AL tiene el ASCII) MOV AL,AH ; Recuperar valor original de AL AND AL,0FH ; Aislar nibble bajo XLAT ; y obtener el ASCII correspondiente JMP PRINT_AL ; Emitir PRINT_AL: PUSH DX ; Salvar DX en la pila XCHG AX,DX ; Intercambiar AX con DX MOV AH,02 ; Función del DOS para emitir ASCII en DL INT 21H ; LLamar al DOS XCHG AX,DX ; Recuperar AX POP DX ; Recuparar DX RET

    Lo primero que hay que saber (decimoseptima parte)

    Los programas .EXE
    En los programas .EXE los registros de segmento pueden tener diferentes valores. Por lo general hay que definir un segmento de datos, otro para el stack y otro para el código. Dentro de un programa puede haber diferentes segmentos de datos y de código. El mismo programa se encarga de dar a DS el valor del segmento correspondiente. Por ejemplo, para definir un segmento de datos se escribe: DATOS SEGMENT ... DATOS ENDS donde DATOS es un nombre simbólico y SEGMENT y ENDS indican al ensamblador los límites del segmento. Luego para dar a DS el valor de DATOS, hay que ejecutar: MOV AX,DATOS MOV DS,AX Conviene resaltar que DATOS no tiene por qué ser un segmento de 64 Kbytes. En realidad los "segmentos" definidos con SEGMENT son *fragmentos* de hasta 64 Kbytes de extensión. La directiva SEGMENT tiene parámetros opcionales que dan información adicional al ensamblador y al linker. Si un programa tiene más de un segmento de código, las rutinas de un segmento sólo pueden ser llamadas desde otro mediante un CALL FAR. La diferencia entre un CALL y un CALL FAR es que éste último aplia CS e IP (segmento y offset de la dirección de retorno). Por ese motivo las rutinas que van a ser accedidas desde segmentos externos deben terminar con la instrucción RETF (RETurn Far). Esta instrucción desapila dos palabras del stack y las coloca en IP y CS. Es importante comprender que dos rutinas en segmentos diferentes pueden estar a una distancia inferior a los 64 Kbytes. Lo que ocurre es que cada una de ellas se ejecutará con valores diferentes del registro CS. Si una rutina termina con RETF deber ser llamada con un CALL FAR aún dentro del mismo segmento. La dirección de memoria donde efectivamente se va a cargar un programa solamente se conoce en el momento mismo de la carga. Por eso el ensamblador no tiene manera de saber cuál va a ser el valor de los nombres de segmentos (ej. DATOS). El ensamblador mantiene los nombres simbólicos de segmento sin valor definido y produce un archivo con terminación .OBJ (OBJect). Estos archivos son procesados por el "Linker". El linkeado resuelve ciertas referencias cruzadas entre nombres de segmentos y arma un "header" (encabezamiento) que define a cada una de esas direcciones como un offset desde el principio del programa. Cuando DOS carga el programa suma la dirección de inicio a los nombres de segmento. La información de los lugares donde debe hacer esto la toma del header. Por este motivo, en relación con los programas .COM, los programas .EXE ocupan más espacio en disco (el correspondiente al header) y se carguan más lentamente en memoria. Cuando DOS carga un programa .EXE en memoria inicializa DS y ES al segmento del primer párrafo asignado al programa. Por lo tanto DS:0 y ES:0 apuntan al comienzo del PSP. Como los dos primeros bytes del PSP contienen la instrucción INT 20H, para terminar un .EXE con una instrucción RET se debe ejecutar al principio del programa lo siguiente: PUSH DS XOR AX,AX PUSH AX Esto manda al final del Stack la dirección (segmento y offset) del comienzo del PSP. Luego el programa puede terminar con un RETF que desapilará esta dirección produciendo un salto a la instrucción INT 20H la cual se encarga de devolver el control al DOS. Como vimos en el caso de los programas .COM, es mejor terminar un programa con la función 4CH de la INT 21H ya que el valor que demos a AL podrá ser leído desde el DOS por la variable ERRORLEVEL. Por ejemplo, un programa que quiera terminar con código de error 0, puede hacerlo con: MOV AX,4C00H INT 21H En el header de un .EXE también se encuentra la dirección a la que DOS transferirá el control una vez cargado el programa. Al contrario que en el caso de los .COM, los programas .EXE pueden comenzar a ejecutarse en cualquier dirección. En algunos assemblers la directiva END sirve para indicar el punto de entrada al programa. Por ejemplo si un programa va a comenzar su ejecución en la rutina MAIN, debemos terminar el código fuente con END MAIN De ese modo al cargarse el programa, CS será inicializado por DOS con el valor del segmento de código al cual pertenece MAIN y el registro IP será inicializado al offset que MAIN tiene dentro de ese segmento.
    Tema: : Numeros Hexadecimales
    @PID: RA 1.11 21280 @MSGID: 0:0/0 500f42e2 Notación Hexadecimal. Un punto de vista computacional. (*) Las computadoras son máquinas con una capacidad descomunal para manejar largas tiras de 0s y 1s. Para ellas la memoria es un conjunto de celdas que se encuentran en dos estados posibles ON y OFF. Estas celdas se llaman bits. El hombre ha aprendido diferentes métodos para codificar información en la memoria de las computadoras. Por ejemplo una imagen en un monitor monocromático es una larga tira de bits cada uno en correspondencia con un pixel del video. Un bit en estado ON significa que el pixel asociado está iluminado y uno en OFF que está apagado. Como vemos, las tiras de bits no siempre representan números y en realidad los programas interpretan solamente una peque¤a porción de la memoria como datos numéricos. Ahora bien, independientemente del significado que pueda tener una tira de bits, una cosa es cierta: es malo para la vista trabajar con cosas como: 1001101110010110 Se puede mejorar un poco el aspecto de este tipo de engendros introduciendo una simple separación: 1001 1011 1001 1110 Pero mejor aún es si ponemos un nombre a cada conjunto de 4 bits. Estos conjuntos se llaman nibbles: 0000: 0 0100: 4 1000:8 1100:C 0001: 1 0101: 5 1001:9 1101:D 0010: 2 0110: 6 1010:A 1110:E 0011: 3 0111: 7 1011:B 1111:F Con estos nuevos símbolos, la tira anterior se escribe: 9 B 9 E Ahora los espacios son más molestos que útiles y podemos poner 9B9E que no es otra cosa que la representación hexadecimal de la tira de bits. Para resaltar el hecho de que estamos usando la codificación hexadecimal, en assembler se usa el sufijo H. Por ejemplo: 9B9EH. Esta convención es especialmente útil cuando la representación hexadecimal no posee letras como en el caso de 91H.
    Resumen:
    1) Cada BYTE tiene 8 BITS. El primer bit (el de la izquierda) es el bit 7, el último es el 0. Ejemplo: 10011011 | | | +------- bit 0 +-------------- bit 7 2) Cada byte tiene 2 NIBBLES. El primer nibble se llama ALTO el otro, BAJO. Ejemplo: 1001 1011 ---- ---- | | | +-------- nibble bajo +------------- nibble alto 3) Una CIFRA HEXADECIMAL es uno de los siguientes símbolos: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F. 4) Cada nibble se puede representar por una cifra hexadecimal. Ejemplos: 1001 se representa por 9 y 1011 por B. 5) Si se necesita interpretar una cifra hexadecimal como un número, entonces A corresponde a 10, B a 11, C a 12, D a 13, E a 14 y F a 15. 6) Si se necesita interpretar un byte como una cantidad se debe multiplicar el nibble alto por 16. Ejemplo: 9B corresponde al número 9*16+11=155. || |+------------- nibble bajo = 11 +-------------- nibble alto * 16 = 144 --- interpretación numérica de 9BH = 155 7) También se usan las PALABRAS (o WORDS) que constan de 2 bytes. El primer byte se llama byte ALTO, el segundo BAJO. Ejemplo: 9B9E | | | +------------ byte bajo +-------------- byte alto 8) En el caso en que dos o más bytes deban ser interpretados como valores numéricos cada uno de ellos se deberá multiplicar por una potencia de 256. Ejemplo: 019B9E | | | | | +------------- byte bajo = 158 | +--------------- byte medio * 256 = 39680 +----------------- byte alto * 256^2 = 65536 ----- 105374 (*) Basado en el capítulo "Digression: A Notation System for Bit Patterns" del minicurso de Assembler "An Assembly Language Primer (C) 1983 by David Whitman". Leandro y Valeria

    Tema: : Trucos en assembler

    @PID: RA 1.11 21280 @MSGID: 0:0/0 500c7dd0 ;--------------------------------; ; Unos pocos TRICKS en Assembler ; ;--------------------------------; ; Los trucos en general *no* son convenientes porque pueden comprometer ; la claridad o la generaldiad del código. Sin embargo algunos trucos ; son tan utilizados que conviene conocerlos de antemano para no ; trabarse cuando se examina el desensamble de una rutina o se lee un ; archivo fuente que no los explica claramente. Otros solucionan de una ; manera elegante y consisa problemas típicos de la programación en ; assembler.
    Ingenuos pero muy difundidos:
    TRICK_0: XOR AX,AX ; Equivale a MOV AX,0 pero es más rápido SUB AX,AX ; Otra variante de lo anterior ... TRICK_1: OR AX,AX ; Usado para ver si AX es cero, par o positivo AND AX,AX ; Otra variante de lo mismo ...
    Sencillos pero piolas:
    TRIC_2: CMP AL,SALIDA_OK ; Para salir con CY en caso STC ; que AL sea diferente de SALIDA_OK JNE RET ; ... TRICK_3: JMP RUTINA_FINAL ; En lugar de: CALL RUTINA_FINAL ; RET
    Ingeniosos:
    TRICK_4: SUB AL,PPIO ; Sirve para salir con NC CMP AL,FIN - PPIO + 1 ; en caso en que AL cumpla la condición CMC ; PPIO <= AL <= FIN y con CY en caso contrario RET
    Peligrosos:
    TRICK_5: CALL VER_SI_SIGO ; Chequear posible error L1: ... ; Proceder como si no hubiera habido error CLC ; Finalmente salir con NC como se¤al de OK RET VER_SI_SIGO: ; Rutina que chequea el error ... ; Código de chequeo propiamente dicho JNC RET ; Si todo OK, salir (vuelve a L1) POP AX ; Si error, quita dirección de retorno del stack RET ; Ahora RETorna a la rutina que llamó a TRICK_5, ; no a L1. Además vuelve con la se¤al CY.
    Simplifican el código:
    TRICK_6: CALL EVALUAR ; Decidir qué hay que poner en AH JE L1 ; Si hay que poner VAL_2, salto a L1 MOV AH,VAL_1 ; Si hay que poner VAL_1, hacerlo DB 0A9 ; El valor 0A9 es el código de TEST AX,... L1: ; donde ... son los 2 bytes ensamblados por MOV AH,VAL_2 ; la instrucción MOV AH,VAL_2 ... ; Si se llegó por L1, entonces AH = VAL_2 ; Si se pasó por DB 0A9, entonces AH = VAL_1 TRICK_7: PUSH SEGUIR ; Debido a este PUSH, la próxima instrucción CMP AH,OPCION_1 ; RET hará que el flujo del programa continúe JE ATENDER_1 ; en la dirección rotulada SEGUIR. CMP AH,OPCION_2 ; Esta es una buena manera atender diferentes JE ATENDER_2 ; opciones antes de continuar con otras cosas. ... ; Si no fuera por el PUSH, habría que haber CMP AH,OPCION_n ; escrito algo como: JE ATENDER_n ; CMP AH,OPCION_1 SEGUIR: ; JNE > L1 ... ; CALL ATENDER_1 ; JMP SEGUIR ; L1: ; CMP AH,OPCION_2 ; ....
    Tema: : Lost Interrupts
    @PID: RA 1.11 21280 @MSGID: 0:0/0 501af2ce COMUNICACION A BAJO NIVEL CON LOS PORTS PARALELOS Exiten 3 registros por lo cuales acceder a un port paralelo, estos son: REGISTRO DIRECCION ------------------------------------------------------- Data Output BASE Status BASE + 1 Control BASE + 2 Donde BASE = 03BCH (MDA, Hércules) 0378H (LPT1) 0278H (LPT2). Un programa puede determinar la dirección de BASE consultando la memoria baja de la BIOS en las direcciones: DIRECCION BASE ----------------- 0:0408 LPT1 0:040A LPT2 El código para hacer esto es el siguiente: GET_BASE: MOV AX,040H ; Estas dos instruciones sirven para que en la MOV ES,AX ; tercera el MOV lea la dirección 0040:0008 MOV DX,ES:[8] ; DX=BASE para LPT1 (poner 10 para LPT2) RET
    Descripción de los registros:
    DATA OUTPUT: Este es el registro que se usa para mandar los datos a la impresora. STATUS: Bits 7 6 5 4 3 2 1 0 | | | | | | | | | | | | | | | +-- \ | | | | | | +---- > No se usan | | | | | +------ / | | | | +-------- 0=Printer error, 1=no error | | | +---------- 0=Printer off line, 1=on line | | +------------ 0=Hay papel, 1=Out of paper | +-------------- 0=ACK (recibo de datos), 1=estado normal +---------------- 0=Busy, 1=Printer not busy CONTROL: Bits 7 6 5 4 3 2 1 0 | | | | | | | | | | | | | | | +-- 0=estado normal, 1=salida de 1 byte (*) | | | | | | +---- 0=estado normal, 1=Automatic LF after CR | | | | | +------ 0=inicializar port, 1=estado normal | | | | +-------- 0=desactivar printer, 1=estado normal | | | +---------- 0=inhibir interrupciones, 1=habilitarlas | | +------------ \ | +-------------- > No se usan +---------------- / (*) En los manuales de las impresoras hay un gráfico que muestra cómo después de mandar un byte de datos al port, se debe mantener baja la se¤al STROBE (que normalmente está alta) durante un lapso que suele ser de 1/2 microsegundo como mínimo. El código para inicializar el port sería: INIT_PORT: INC DX ; Se supone que DX contiene la dirección BASE INC DX ; Control Registre = BASE + 2 MOV AL,08H ; 018H si se desea habilitar las interrupciones OUT DX,AL ; Enviar el valor de inicialización al port L1: ; Bucle de espera 4096 veces MOV AX,01000H ; Inicializar contador DEC AX ; decrementar en 1 JNZ L1 ; Jump Not Zero a L1 MOV AL,0CH ; 01CH si se desea habilitar las interrupts OUT DX,AL ; Enviar el valor normal al port. RET Como su nombre lo indica el registro de Status sirve para checkear el funcionamiento de la impresora. Ejemplo: Ver si la impresora está On Line y tiene papel: TEST: IN AL,DX ; Se supone que DX = BASE + 1 TEST AL,BIT 4 ; Consultar bit 4 del Status Register JZ OFF_LINE ; Jump if Zero a tratar el error TEST AL,BIT 5 ; Consultar bit 5 del Status Register JNZ OUT_OFF_PAPER ; Jump if Zero a tratar el error RET Ejemplo: Enviar un byte de datos al port paralelo. PRINT_AL: OUT DX,AL ; Se supone que DX = BASE y que AL = dato INC DX ; Pasar al port con dirección BASE + 2 INC DX ; para acceder al Control Register MOV AL,0DH ; Bit 0 = 1 : Strobe bajo. 1DH si interrupts DEC AL ; Bit 0 = 0 : Strobe alto. OUT AL,0CH ; Enviar al port fin de se¤al "ahí fue el dato" RET
    Mensaje #11635 - Assembler 80x86 Fecha: 02-08-92 21:37 De : Leandro Caniglia Para: All Tema: : lost interrupts @PID: RA 1.11 21280 @MSGID: 0:0/0 501af302 Hola a todos. Como en los últimos mensajes nos enganchamos con el tema de las interrupciones de la impresora, les paso la documentación que encontré al respecto. Los que no tienen familiaridad con el assembler no arruguen que es fácil! Otra cosa: la información y las rutinas de este mensaje constituyen ejemplos de programación a bajo nivel. OJO porque he visto como en la FCEyN hay [... no se que término emplear... bue' digamos] docentes que le llaman programación de bajo nivel a cualquier engendro escrito en assembler. Aclaro esto para que nadie crea que para imprimir un simple string por la impresora, un programador en assembler debe manejar toda esta información. No, para imprimir el string PIRULO lo único que hay que hacer es esto: DATOS DB 'PIRULO' ; String a imprimir LONG EQU $ - OFFSET DATOS ; LONG es una constante = Long(string) PRINT_PIRULO: MOV BX,OFFSET DATOS ; BX = Dirección del string MOV CX,LONG ; CX = Long del string JMP PRINT ; A la rutina de impresión PRINT: MOV AH,05 ; Servicio nro 5 del DOS L1: MOV DL,[BX] ; DL = caracter a imprimir INT 021 ; imprimirlo INC BX ; apuntar al próximo caracter LOOP L1 ; El bucle se repite CX veces RET
    Tema: Segmentación
    @PID: RA 1.11 21280 @MSGID: 0:0/0 50303aa6
    Segmentación
    Este mensaje es una digresión de los mensajes sobre registros de segmento que están bajo el subject "Lo primero que hay que saber..." Para más detalles sobre segmentación ver las partes quinta y siguientes. ----------------------------- En el 8088 las direcciones absolutas de memoria se obtienen como un OFFSET o desplazamiento de hasta 64 Kilo bytes (65535 bytes) dentro de un SEGMENT. Esta es una característica del direccionamiento *segmentado* que no se encuentra en otros micros. Por ejemplo la COMMODORE 128 tiene (esencialmente) el mismo micro que la 64 y sin embargo puede direccionar más memoria. La técnica que se usó en la C128 es un poco diferente a la del 8088. En la 128 las direcciones se componen de 4 cifras hexa (16 bits). Esto, al igual que en la 64, da un espacio de direccionamiento de 64 Kbytes: la primera dirección es la 0000H y la última la FFFFH = 65535 decimal. Lo que se hizo en la 128 es agregar un chip llamado MMU (Memory Managment Unit) que sirve para elegir una configuración de bancos. Físicamente existen 128 Kbytes de RAM y se puede programar la MMU para que la CPU "vea" 64 de esos 128 K en cada momento. Esto se logra mediante una partición lógica de la memoria disponible en porciones más peque¤as. La MMU "elige" algunas de estas porciones y forma los 64 K que le hace ver a la CPU. El programador maneja esto programando la MMU para indicar la configuración elegida. Si en un momento necesita acceder a datos que se encuentran en una porción que está fuera de los 64 K activos, puede cambiar la configuración para que ese sector de memoria ingrese en el campo visual de la CPU, dejando otro temporalmente fuera del mismo. Esto que dicho así, mal y pronto, puede parecer complicado es en realidad muy sencillo de manejar y cuando uno comprende los detalles lo incorpora rápidamente a su forma de pensar. LLegado ese punto deja de ser un problema y uno puede programar concentrado en lo realmente importante. La diferencia entre lo que acabamos de describir y la segmentación de memoria del 8088 es que mientras en la C128 las configuraciones posibles (por bancos) son unas pocas (debido a limitaciones propias de la MMU), en el 8088 el comienzo de un segmento puede elegirse con absoluta libertad. La única restricción es que su dirección absoluta sea múltiplo de 16 (010H). De este modo se logra un manejo mucho más flexible de la memoria. Aquí no tenemos configuraciones rígidas de bancos predefinidos, tenemos una gran libertad de elegir el inicio del segmento de 64 Kbytes. Y una vez elegido un segmento, podemos movernos en él con direcciones de 16 bits como si se tratara de una computadora de 64 Kbytes. Más aún, las características fundamentales de la segmentación son varias: 1) El origen de los segmentos se fija desde la CPU a través de los registros de segmento (y no mediante el manejo de chips especiales como el MMU de la C128) 2) En cada momento uno tiene 4 bloques de 64 Kbytes visibles por el 8088: uno con origen en cada uno de los registros de segmento DS, ES, CS, SS. 3) Cada segmento puede ser elegido con total libertad con la única restricción de que su dirección absoluta (de 20 bits) termine con la cifra hexadecimal 0 (es decir, sea múltiplo de 16) 4) La segmentación no impide que paralelamente se puedan manejar bancos de memoria a través de chips especiales tipo MMU. De hecho hay zonas altas de memoria en las que chips de RAM y de ROM comparten el mismo direccionamiento (Shadow RAM) y el swapping entre uno y otro banco se maneja a través de la programación de esos dispositivos. Hoy en día, la segmentación puede parecer una limitación frente a otras formas de direccionamiento directas en las que la CPU mantiene en su campo visual toda la memoria disponible. Sin embargo hay que entenderla como lo que realmente es: un avance con respecto a micros que manejaban espacios de direcciones de 64 Kbytes. Ya en la C64 hay en realidad más de 64 Kbytes de memoria y un programador puede configurar el espacio de direccionamiento para que la CPU vea RAM o CHAR ROM o SYSTEM ROM o BASIC ROM o una combinación de ellas. Los que entendieron esto en su oportunidad lo aprovecharon y cuando pasaron a la C128 puedieron ver las ventajas del manejo de bancos que esta ofrecía. Del mismo modo, es posible aprovechar la experiencia adquirida en máquinas como la C128 para comprender y valorar la "solución" de la segmentación que permite un manejo sumamente flexible de 1 Mb de memoria en un micro cuyos registros son de 16 bits. Hoy en día la segmentación conserva su vigencia por la inercia producida por millones de programas que corren bajo DOS, pero es una forma de trabajo en extinción. Sin embargo, puede ser importante entenderla y manejarla para no pasar por alto otra etapa de la evolución de la computación.
    Tema: : Alguna cosita sobre instrucciones no docuentadas
    @PID: RA 1.11 21280 @MSGID: 0:0/0 503ec330 Hola a todos. Charlando con Ricardo Pesce surgió el tema de las instrucciones no documentadas del 80x86. Supongo que todas las CPU son capaces de hacer cosas que nunca se vuelcan en la documentación final. Gracias a las personas que trabajan en el desarrollo del microcódigo, muchas veces podemos conocer algunas de esos misterios. El microprocesador 80286 tiene dos modos de funcionamiento: Real y Protegido. En el modo Real la 286 es 8086 rápido con unas pocas instrucciones más, que también existen en el 80186. El DOS trabaja en modo real. La gran diferencia entre el 80286 y sus antecesores 8088, 8086 y 80186 es la habilidad que tiene de pasar al modo protegido que posee facilidades para la multitarea y tiene un modo de direccionamiento que le permite direccionar 16 Mb (contra 1 Mb del modo real). Hay un servicio de la INT 15H de la BIOS de la AT que permite pasar al modo protegido. Oficialmente la única manera de volver del modo protegido al real es reseteando. Sin embargo Mauricio Taslik me contó que existe una instrucción no documentada que permite volver del protegido al real. Parece ser que ciertos programas que necesitan manejar grandes volúmenes de datos en memoria la utilizan. En el manual del A86, el autor comenta en el capítulo 5 dos instrucciones que si bien son conocidas, tienen variantes no documentadas muy interesantes. Estas variantes están disponibles en el A86-D86. Las instrucciones son AAM y AAD. Según la documentación estas instrucciones hacen lo siguiente: AAM AH <- AL DIV 10 AL <- AL MOD 10 donde AL DIV 10 es el valor entero de la división de AL por 10 y AL MOD 10 es el resto de esa división. AAD AL <- AH*10 + AL AH <- 0 La primera sirve para convertir un valor binario a decimal. La segunda hace el trabajo inverso. Por ejemplo: Si AL = 1BH, después de ejecutar la instrucción AAM queda: AH = 02H y AL = 07H, ya que 1BH = 16 + 11 = 27. Esto es comodísimo para mostrar el valor decimal de AL. Ejecutando: MOV AL,1BH AAM OR AX,3030H RET A la salida de la rutina, AH contiene el valor 32H, que es el ASCII de '2' y AL el valor 37H, que es el ascii de '7'. Con AAD podemos hacer el trabajo contrario. Convertir una cifra decimal a su valor hexadecimal: MOV AH,'2' MOV AL,'7' AND AX,0F0FH AAD RET Y a la salida de la rutina, AH contendrá el valor 1BH. Una vez ensambladas, la instrucciones AAM y AAD ocupan 2 bytes de memoria. AAM produce los bytes D4 0A y AAD los bytes D5 0A. Esto puede verse con ayuda del DEBUG. En el ensamble de toda instrucción del lenguaje máquina aparecen tantos bytes como sean necesarios para contener toda la información de la instrucción y sus operandos. En el caso de AAM y AAD no tenemos operandos (según la documentación oficial). Por lo tanto es innecesario que ocupen 2 bytes cada una. El byte que estaría de más es 0A. Pero 0A es justamente el valor 10 con el que estas instrucciones operan. Si fuera una constante para las operaciones no aparecería ahí. Bueno ocurre que si uno reemplaza 0A por otro valor, las instrucciones hacen las cuentas con ese valor. Lo que se explica en el manual del A86 es que este ensamblador permite escribir AAM operando y AAD operando, donde "operando" es un operando de 1 byte. El A86 se encarga de ensamblar este operando a continuación de D4 y D5. Si uno prueba a ensamblar desde el DEBUG un operando a continuación de las instrucciones AAM y AAD obtiene un mensaje de error. Una aplicación interesante de estas extensiones no documentadas es cuando el "operando" es 16. En ese caso AAM se ensambla los bytes D4 10 y AAD los bytes D5 10 (estoy escribiendo en hexa, 10 es 16 decimal). Si uno no quiere usar el A86 puede ir al DEBUG, ingresar el comando A(ssemble) y a continuación escribir AAM o AAD. Luego de dar enter 2 veces, mirar los bytes ensamblados con el comando D(ump). Ahí uno comprueba lo que dije del D4 0A y el D5 0A. Después con el comando E(dit) uno puede cambiar 0A por 10 (16 decimal). Desensamblando con U(nenssamble), se obtiene: ????:0100 D410 AAM 10 ????:0102 D510 AAD 10 Esto significa que el DEBUG sabe desensamblar los operandos aunque no los sabe dessensamblar. Bien. Y por qué el valor 16 es un reemplazo interesante del 0A? Por lo siguiente. La rutina: MOV AL,1BH AAM 10H RET devuelve AH = 01H el nibble alto de AL AL = 0BH el nibble bajo de AL Sin el uso de esta instrucción deberíamos haber hecho el siguiente despliegue para obtener el mismo resultado: MOV AH,AL ; Salvar AL en AH SHR AH,1 SHR AH,1 SHR AH,1 SHR AH,1 AND AL,0FH RET En una 186, 286, 386 o 486 podemos poner SHR AH,4 en lugar de los 4 SHR AH,1 pero igual es más complicado. En el 8088 no exite el SHR AH,4. Esto es muy útil para imprimir en pantalla el contenido del registro AL ya que para mostrar las dos cifras hexadecimales que lo forman uno debe aislar cada uno de los nibbles, convertirlos a ASCII y después emitirlos. (En un mensaje de la serie "Lo primero que hay que saber..." esto está explicado con detalle. Con respecto al AAD, el operando 16 sirve para hacer el camino inverso: convertir dos nibbles en AH y AL en un byte en AL: MOV AH,01H MOV AL,0BH AAD 16 RET y a la salida AH = 0 AL = 1BH Sin este truco tendríamos que haber escrito: MOV AH,01H SHL AH,1 SHL AH,1 SHL AH,1 SHL AH,1 OR AL,AH XOR AH,AX RET Como antes, en una 186+, podríamos haber usado SHL AH,4 que no existe en el 8088/86. El autor del A86 advierte sin embargo que AAD no adimite el cambio de operando en los microprocesadores NEC V20 y V30 pero que AAM sí funciona. Yo probé en un NEC V20 y lo comprobé. Bueno, esto es todo lo que se hacerca de instrucciones no documentadas, aunque no es mucho me tomé la libertad de explayarme como una escusa para repasar otras cositas.
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    Instrucciones del Procesador 8088 Intel
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ AAA Ajuste ascii para la suma AAD Ajuste ascii para la diision AAM Ajuste ascii para la multiplicacion AAS Ajuste ascii para la resta ADC Suma con arraste ADD Suma AND Y logico CALL Llamada CBW Convierte byte a palabra CLC Borra el indicador de arrastre CLD Borra el indicador de interrupcion CLI borra el inidicador de interrupcion CMC Complementa el inidicador de arrastre CMP Compara CMPS Compara byte o palabra (de cadena) CMPSB Compara cadena e bytes CMPSW Compara palabra o doble palabra CWD Convierte palabra a doble palabra DAA Ajuste decimal para la suma DAS Ajuste decimal para la resta DEC Decrementa DIV Divide ESC Escape HLT Alto IDIV Divide enteros IMUL Multiplica enteros IN Entre byte o palabra (de un port) INC Incrementa INT Interrupcion (ejecuta) INTO Interrumpe por desbordamiento IRET Vuelta de una interrupcion JA Salta si superior JAE Salta si superior o igual JB Salta si inferior JBE Salta si inferior o igual JC Salta si arrastre JCXC Salta si CX=0 JE Salta si igual JG Salta si mayor JGE Salta si mayor o igual JL Salta si menor que JLE Salta si menor que o igual JMP Salta JNA Salta si no superior JNAE Salta si no superior o igual JNC Salta si no arrastre JNE Salta si no igual JNG Salta si no mayor JNGE Salta si no mayor o igual JNL Salta si no menor JNLE Salta si no menor o igual JNO Salta si no desbordamiento JNP salta si no paridad JNS Salta si no signo JNZ Salta si no cero JO Salta si desbordamiento JP Salta si paridad JPE Salta si paridad par JPO Salta si paridad impar JS Salta si signo JZ Salta si cero LAHF Carga AH con indicadores LDS Carga puntero en DS LEA Carga direccion efectiva LES Carga puntero en ES LOCK Bloquea BUS LODS Almacena byte o palabra (de cadena) LODSB Almacena byte (cadena) LODSW almacena palabra (cadena) LOOP Bucle LOOPE Bucle mientras igual LOOPNE Bucle mientras no igual LOOPNZ Bucle mientras no cero LOOPZ Bucle mientras cero MOV Mueve MOVS Mueve byte o palabra (de cadena) MOVSB Mueve byte (de cadena) MOVSW Mueve palabra (cadena) MUL Multiplicar NEG Niega NOP No operacion NOT Negacion logica OR O logico OUT Salida de byte o palabra (por un port) POP Desapila POPF Desapila indicadores PUSH Apila PUSHF Apila inidicadores RCL Rotacion a la izquierada contando con el arrastre RCR Rotacion a la derecha contando con el arrastre REP Repite RET Retorno RDL Rotacion izquierda ROR Rotacion Derecha SAHF Almacena AH en los indicadores SAL Desplazamiento aritmetico a la izquierda SAR Desplazamiendo aritmetico a la derecha SSB Resta con acarreo SCAS Busca byte o palabra (de cadena) SCASB Busca byte (cadena) SCASW Busca palabra (cadena) WAIT Espera SLAT Traslada SHL Desplazamiento a la izquierda SHR Desplazamiento a la derecha STC Pone indicador de arrastre STD Pone indicador de direccion STI Pone indicador de interrupcion STOS Almacena byte o palabra (de cadena) STOSB Almacena byte (cadena) STOSW Almacena palabra (cadena) SUB Resta TEST Test XCHG Intercambio (Swap) XOR O exclusivo Logico
    2 Libros para leer: - Guia del programador para el IBM PC. - Guia del programador en ensamblador para el IBM PC. Ambos de Peter Norton.. la editorial es ANAYA, pero.. los conseguir por todos lados, en fotocopias (el de assembler, sale 120$, por eso, fotocopialo) Despues de leerte esos 2 libros.... vas a entender todo.. De cualquier manera, lista de interrupciones, te conviene conseguirte la q/ronda por varios bbs/s (Esta en Esabb) q/tiene interrupciones NO DOCUMENTADAS... Es decir, q/ni Peter Norton, te las dice... y q/con esas, les podes sacar bastante jugo a las Pcs...
    Tema: : Algo es algo
    ; Este programa, así como está, debe ser compilado usando el A86. ; El paquete del A86 está en el area de files de assembler. ; El programa solamente anda en una AT. En una XT no hace nada. ;-----------------------------------------------------; ; Si tiene alguna duda mande un mensaje. ; ; Buena suerte ; ;-----------------------------------------------------; JMP MAIN ; Saltar por encima de los datos MSG: DB 'FASTKBD V1.0 (1991) - por Leandro Caniglia',10,13 DB ' Con FASTKBD el cursor se mueve MUY RAPIDO.',10,13 DB '$' MAIN: CALL SHOW_MSG ; Imprimir mensaje en pantalla MOV AH,03 ; Número de función de la BIOS MOV AL,05 ; Número de subfución MOV BH,00 ; Retardo mínimo: 250 milisegundos MOV BL,00 ; Ratio mínimo de typematic: 30 caracters/seg INT 22 ; Interrucpión de la ROM BIOS RET ; Fin del programa, vuelta al DOS SHOW_MSG: MOV DX,MSG ; DS:DX apunta al comienzo del string MOV AH,09 ; Función Print String del DOS INT 33 ; Imprimir el string RET ; Vuelta al punto de llamada ; Algunas Explicaciones: ; El JMP MAIN del principio es obligatorio porque cuando DOS transfiere ; el control a un programa .COM lo hace mediante un salta al principio ; de ese programa. Uno quiere que se ejecute la ruitna MAIN y no que ; se interprete el mensaje como código ejecutable. ; La instrucción INT 22 es una llamada a la BIOS. A través de ella se ; puede acceder a diferentes funciones de control del teclado. La número ; 03 sirve para establecer la velocidad de respuesta de las teclas. ; El valor en BH (retardo) sirve para determinar el tiempo que hay que ; mantener apretada una tecla para que entre en el modo de repetición. ; El valor en BL (Typematic Ratio) es la velocidad de repetición de ; la tecla que se mantiene apretada. ; La instrucción INT 33 es una llamada al DOS. Es la puerta de ; acceso a todas las funciones del DOS (abrir y cerrar archivos, ; formatear discos, etc, etc) ; La función 09 sirve para imprimir un string en pantalla. El string ; debe terminar con el caracter '$' (que no es mostrado en pantalla).
    Tema: : Programacion a bajo nivel
    ;------------------------------------------------------; ; DESENSAMBLAR LA BIOS ES UNA BUENA MANERA DE APRENDER ; ;------------------------------------------------------; ; ** TODOS LOS NUMEROS ESTAN ESCRITOS EN BASE HEXADECIMAL ** ; ** ENSAMBLAR CON EL A86, EL PAQUETE COMPLETO ESTA EN LA SECCION FILES ; DE ESTA AREA ** ;INTRODUCCION: ; El TypeMatic es el proceso por el cual después de mantener pulsada una ; tecla durante un cierto período (Delay), el teclado entra en un modo de ; repetición en el que *simula* que el operador pulsa y suelta la tecla ; a una velocidad dada (Repeat Rate). ; La BIOS de la AT contiene el código necesario para programar el chip ; del teclado y ajustar los valores del Delay y del Repeat Rate. ; Si bien este código puede invocarse fácilmente mediante la función 03 ; de la INT 016 con el subservicio 05, es interesante ver los detalles ; que lleva a cabo dicho subservicio. ; Como regla general, los valores que se envían al port 064 corresponden ; a comandos que sirven para programar el chip del teclado. ; Si estos comandos necesitan de datos adicionales, los valores son ; enviados al port 060. ; El comando para definir el comportamiento del TypeMatic tiene el ; número 0F3. Los datos que necesita son el Delay y el Repeat Rate. ; Esos datos se combinan en un byte (que se manda la port 060) de la ; siguiente manera: ; bits 7 : reservado (siempre 0) ; 6,5 : Delay: 00=250ms, 01=500ms, 10=750ms, 11=1seg ; 4,3,2,1,0 : Repeat Rate: de los 32 posibles valores ; los más importantes son los que siguen ; (los demás salen por interpolación) ; 0=30.0 0A=10.0 ; 1=26.7 0D= 9.2 ; 2=24.0 010= 7.5 ; 4=20.0 014= 5.0 ; 8=15.0 01F= 2.0 ; repeticiones/seg ; ; El comando 0F4 se llama Enable Keyboard. Al enviarlo el teclado ; devuelve una se¤al de ACKnowledge, limpia sus buffers internos y ; reinicia el scanneado de la matriz del teclado. ; Otra cosa a tener en cuenta es la variable de la BIOS ubicada en la ; dirección 040:097. Es una variable de 1 byte que contiene en sus bits ; la siguiente información del teclado: ; bits 7 : 1=Error ; 6 : Mode Indicator Update (?) ; 5 : Resend Receive Flag (?) ; 4 : Received ACKnowledge ; 3 : Reservado ; 2,1,0 : Estado de los LEDs del teclado ;-------------------------------------; ; PROGRAMACION DEL TYPEMATIC ; ; Entran: BH = Delay = (0..3) ; ; BL = Repeat Rate = (0..01F) ; ;-------------------------------------; SBT_08C16: STI ; Habilitar interrupciones de Hardware PUSH DS,BX,CX ; Salvar registros estropeados PUSH 040 ; DS = 040 (Zona de variables de la BIOS) POP DS ; a traves del stack CMP BL,01F ; BL = Repeat Rate debe estar entre 0 y 01F JA > L1 ; Rechazar si inválido CMP BH,03 ; BH debe tener únicamente 2 bits JA > L1 ; Rechazar si inválido ROR BH,3 ; Manda bits 0,1 a 5,6 para obtener Delay OR BL,BH ; BL = Delay y Repeat Rate CALL SBT_08AA4 ; Mandar Comando 0AD (?) al port 064 STI ; Habilitar interrupciones por Hardware MOV AL,0F3 ; Valor para definir el TypeMatic CALL SBT_08B9B ; Mandar comando 0F3 al port 060 MOV AL,BL ; AL = datos de TypeMatic CALL SBT_08B9B ; Mandar los datos al port 060 CALL SBT_08A9B ; Mandar comando 0AE (?) al port 064 STI ; Habilitar interrupciones por Hardware MOV AL,0F4 ; Valor para Enable Keyboard (Habilita Teclado) CALL SBT_08B9B ; Mandar el comando L1: POP CX,BX,DS ; Restaurar registros RET ;----------------------------; ; ATENCION! ; ; La rutina que sigue inhibe ; ; el teclado al mandar el ; ; valor 0AD al port 064 ; ;----------------------------; SBT_08AA4: CALL SBT_08AB1 ; Leer port 064 MOV AL,0AD ; Enviar comando 0AD (?) OUT 064,AL ; al port 064 CALL SBT_08AB1 ; Leer port 064 IN AL,060 ; Leer port 060 RET SBT_08AB1: CLI ; Inhibir interrupciones por Hardware XOR CX,CX ; CX = contará 65.536 veces como máximo L1: ; Bucle de espera JMP > L2 ; Salto que sirve para esperar un poco L2: ; antes de volver a leer el port. IN AL,064 ; Leer port 064 TEST AL,02 ; Consultar bit 1 (?) LOOPNZ L1 ; Si apagado continuar esperando JZ RET ; Cuando bit 1 apagado, salir (?) RET SBT_08B9B: MOV AH,AL ; Salvar dato en AH MOV CX,03 ; Intentar hasta 3 veces L1: PUSH CX ; Salvar contador de veces (hasta 3) CALL SBT_08AB1 ; Consultar port 064 MOV AL,AH ; Recuperar dato OUT 060,AL ; Mandar el dato al port 060 AND B[097],04F ; Apagar bits 7 (Error), 5 (Resend Receive Flag) ; y 4 (Received ACKnowledge) STI ; Habilitar interrupciones XOR CX,CX ; CX = contará 65.536 veces como máximo L2: TEST B[097],030 ; Consultar Resend Receive Flag & ACK JNZ > L4 ; Si alguno encendido continuar LOOP L2 ; Esperar L3: TEST B[097],030 ; Si no hubo respuesta esperar LOOPZ L3 ; hasta 65.536 veces más L4: CLI ; Inhibir interrupciones POP CX ; Recuperar contador de (hasta 3) intentos TEST B[097],010 ; Se recibió ACKnowledge del teclado? LOOPZ L1 ; No, nuevo intento si cabe JNZ RET ; Sí, volver (OK) OR B[097],080 ; Tres intentos fallidos: activar bit de error RET SBT_08A9B: CALL SBT_08AB1 ; Consultar port 064 MOV AL,0AE ; Comando 0AE (?) OUT 064,AL ; Enviar el comando STI ; Habilitar interrupciones RET ; Notas: ; 1. Los comentarios marcados con (?) corresponden a cosas que no ; comprendo. Si alguien tiene la información que falta para completar ; esto, por favor envíela al BBS. ; 2. La BIOS de la XT no posee estas rutinas. Por eso no pueden usarse ; los subservicios de la función 03 de la INT 016 para programar la ; velocidad de TypeMatic. Sería bueno que alguien ensamble esto, lo ; pruebe en una XT y luego cuente cómo le fue. Ojo porque puede haber ; diferencias con los ports 060 y 064 entre XT y AT. ; 3. Hay otros subservicios interesantes en la función 03 de la INT 016. ; Si alguien hace un estudio de los mismos, hágamelo saber. ; 4. Las únicas diferencias que tienen estas rutinas con las de la BIOS ; son: ; a) Se evitaron los JMPs iniciales propios de las rutinas en ROM ; b) Se eliminó la comprobación de los valores de AH y AL que ; contienen los números de función y subservicio de la INT 016 ; c) Se reemplazó el IRET por un RET común -------------------------------------------------------------------------------
    Tema: : Otro ejemplito a propósito de un mensaje
    ------------------------------------------------------------------------------- ; En un mensaje dirigido a ALL en el área general, Norberto Olari pidió ; una fórmula o un programa en cualquier lenguaje para calcular el día ; de la semana en que cae una fecha dada. ; Bien, el programa que mando a continuación sirve para calcular el día ; de la semana de una fecha entre 1980 y 2099. ; No usa ninguna fórmula para calcular el día ya llama a las funciones ; 2AH y 2BH de la INT 21H (DOS) que hacen el trabajo (de ahí la ; limitación en la amplitud de las fechas. ; Existe sin embargo una fórmula para calcular el día de la semana de ; cualquier a¤o (aguna vez hice ese ejercicio en el BASIC de la ; COMMODORE). Si alguien tiene ganas y necesita algún dato, le ofrezco ; mi ayuda. Sería (cómo éste) uno de esos ejercicios que hay que hacer ; alguna vez en la vida: buenos para escribir pero raramente necesarios. JMP MAIN ANIO DW ? ; Poner aquí el a¤o entre 1980 y 2099 MES DB ? ; Un byte para el mes DIA DB ? ; Un byte para el día DIA_DE_LA_SEMANA DB ? ; Aquí sale 0=domingo, 1=lunes,... ; 0FFH=fecha no válida MAIN: CALL GET_SYS_DATE ; Leer fecha del sistema PUSH CX,DX ; salvar en la pila CALL SET_SYS_DATE ; Cambiar fecha de hoy x fecha dada CALL GET_SYS_DATE ; Si fecha válida leer día de la semana MOV DIA_DE_LA_SEMANA,AL ; salvar en variable de memoria POP DX,CX ; Recuperar fecha de hoy y CALL SET_SYS_DATE ; restablecerla. MOV AL,DIA_DE_LA_SEMANA ; Mandar el resultado para que quede MOV AH,04CH ; en la variable ERRORLEVEL del DOS INT 021H ; y salir. GET_SYS_DATE: MOV AH,02AH ; Función del DOS para leer fecha INT 021H ; LLamada al DOS RET SET_SYS_DATE: MOV AH,02BH ; Función del DOS para definir fecha MOV CX,ANIO ; CX = a¤o entre 1980 y 2099 MOV DH,MES ; DH = mes MOV DL,DIA ; DL = día INT 021H ; LLamar al DOS RET
    Mensaje #12163 - Assembler 80x86 Fecha: 19-08-92 01:03 De : Leandro Caniglia Para: All Tema: : Otro ejemplito a propósito de un mensaje @PID: RA 1.11 21280 @MSGID: 0:0/0 50303b68 ; En un mensaje dirigido a ALL en el área general, Norberto Olari pidió ; una fórmula o un programa en cualquier lenguaje para calcular el día ; de la semana en que cae una fecha dada. ; Bien, el programa que mando a continuación sirve para calcular el día ; de la semana de una fecha entre 1980 y 2099. ; No usa ninguna fórmula para calcular el día ya llama a las funciones ; 2AH y 2BH de la INT 21H (DOS) que hacen el trabajo (de ahí la ; limitación en la amplitud de las fechas. ; Existe sin embargo una fórmula para calcular el día de la semana de ; cualquier a¤o (aguna vez hice ese ejercicio en el BASIC de la ; COMMODORE). Si alguien tiene ganas y necesita algún dato, le ofrezco ; mi ayuda. Sería (cómo éste) uno de esos ejercicios que hay que hacer ; alguna vez en la vida: buenos para escribir pero raramente necesarios. JMP MAIN ANIO DW ? ; Poner aquí el a¤o entre 1980 y 2099 MES DB ? ; Un byte para el mes DIA DB ? ; Un byte para el día DIA_DE_LA_SEMANA DB ? ; Aquí sale 0=domingo, 1=lunes,... ; 0FFH=fecha no válida MAIN: CALL GET_SYS_DATE ; Leer fecha del sistema PUSH CX,DX ; salvar en la pila CALL SET_SYS_DATE ; Cambiar fecha de hoy x fecha dada CALL GET_SYS_DATE ; Si fecha válida leer día de la semana MOV DIA_DE_LA_SEMANA,AL ; salvar en variable de memoria POP DX,CX ; Recuperar fecha de hoy y CALL SET_SYS_DATE ; restablecerla. MOV AL,DIA_DE_LA_SEMANA ; Mandar el resultado para que quede MOV AH,04CH ; en la variable ERRORLEVEL del DOS INT 021H ; y salir. GET_SYS_DATE: MOV AH,02AH ; Función del DOS para leer fecha INT 021H ; LLamada al DOS RET SET_SYS_DATE: MOV AH,02BH ; Función del DOS para definir fecha MOV CX,ANIO ; CX = a¤o entre 1980 y 2099 MOV DH,MES ; DH = mes MOV DL,DIA ; DL = día INT 021H ; LLamar al DOS RET