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".
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=0Conviene 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.
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.
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 CHay otros flags y los vamos a ver en el próximo mensaje.
;-----------------------; ; 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:;--------; ; 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 FFFDVALOR_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 bytesLa 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.
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
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