-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathMASM-01.txt
988 lines (805 loc) · 63.9 KB
/
MASM-01.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
EL LENGUAJE ENSAMBLADOR DEL 80x86
Hasta ahora hemos visto los mnemónicos de las instrucciones que pasadas a su correspondiente código binario ya puede entender el
microprocesador. Si bien se realiza un gran avance al introducir los mnemónicos respecto a programar directamente en lenguaje
maquina -es decir, con números en binario o hexadecimal- aún resultaría tedioso tener que realizar los cálculos de los desplazamientos
en los saltos a otras partes del programa en las transferencias de control, reservar espacio de memoria dentro de un programa
para almacenar datos, etc... Para facilitar estas operaciones se utilizan las directivas que indican al ensamblador qué debe hacer
con las instrucciones y los datos.
Los programas de ejemplo de este libro y la sintaxis de ensamblador tratada son las del MASM de Microsoft y el ensamblador de IBM.
No obstante, todos los programas han sido desarrollados con el Turbo Assembler 2.0 de Borland (TASM), compatible con el clásico
MASM 5.0 de Microsoft pero más potente y al mismo tiempo mucho más rápido y flexible. TASM genera además un código más reducido y
optimizado. Por otra parte, MASM 5.0 no permite cambiar (aunque sí la 6.0) dentro de un segmento el modo del procesador: esto
conlleva el riesgo de ejecutar indeseadamente instrucciones de 32 bits al no poder acotar exactamente las líneas donde se desea
emplearlas, algo vital para mantener la compatibilidad con procesadores anteriores. También es propenso a generar errores de fase
y otros similares al tratar con listados un poco grandes. Respecto a MASM 6.0, el autor de este libro encontró que en ocasiones
calcula incorrectamente el valor de algunos símbolos y etiquetas, aunque es probable que la versión 6.1 (aparecida sospechosa e
inusualmente muy poco tiempo después) haya corregido dichos fallos, intolerables en un ensamblador. Por otro lado, las posibilidades
adicionales de TASM no han sido empleadas por lo general. Muchos programas han sido ensamblados una vez con MASM, para asegurar
que éste puede ensamblarlos.
Conviene decir aquí que este capítulo es especialmente arduo para aquellos que no conocen el lenguaje ensamblador de ninguna máquina.
La razón es que la información está organizada a modo de referencia, por lo que con frecuencia se utilizan unos elementos -para explicar
otros- que aún no han sido definidos. Ello por otra parte resulta inevitable también en algunos libros más básicos, debido a la
complejidad de la sintaxis del lenguaje ensamblador ideada por el fabricante (que no la del microprocesador). Por ello, es un buen
consejo actuar a dos pasadas, al igual que el propio ensamblador en ocasiones: leer todo una vez primero -aunque no se entienda del
todo- y volverlo a leer después más despacio.
5.1. - SINTAXIS DE UNA LÍNEA EN ENSAMBLADOR.
Un programa fuente en ensamblador contiene dos tipos de sentencias: las instrucciones y las directivas. Las instrucciones se
aplican en tiempo de ejecución, pero las directivas sólo son utilizadas durante el ensamblaje. El formato de una sentencia de
instrucción es el siguiente:
[etiqueta] nombre_instrucción [operandos] [comentario]
Los corchetes, como es normal al explicar instrucciones en informática, indican que lo especificado entre ellos es opcional,
dependiendo de la situación que se trate.
Campo de etiqueta. Es el nombre simbólico de la primera posición de una instrucción, puntero o dato. Consta de hasta 31
caracteres que pueden ser las letras de la A a la Z, los números del 0 al 9 y algunos caracteres especiales como
«@», «_», «.» y «$».
Reglas:
- Si se utiliza el punto «.» éste debe colocarse como primer carácter de la etiqueta.
- El primer carácter no puede ser un dígito.
- No se pueden utilizar los nombres de instrucciones o registros como nombres de etiquetas.
las etiquetas son de tipo NEAR cuando el campo de etiqueta finaliza con dos puntos (:); esto es, se considera cercana:
quiere esto decir que cuando realizamos una llamada sobre dicha etiqueta el ensamblador considera que está dentro del
mismo segmento de código (llamadas intrasegmento) y el procesador sólo carga el puntero de instrucciones IP. Téngase en
cuenta que hablamos de instrucciones; las etiquetas empleadas antes de las directivas, como las directivas de definición
de datos por ejemplo, no llevan los dos puntos y sin embargo son cercanas.
Las etiquetas son de tipo FAR si el campo de etiqueta no termina con los dos puntos: en estas etiquetas la instrucción
a la que apunta no se encuentra en el mismo segmento de código sino en otro. Cuando es referenciada en una transferencia
de control se carga el puntero de instrucciones IP y el segmento de código CS (llamadas intersegmento).
Campo de nombre. Contiene el mnemónico de las instrucciones vistas en el capítulo anterior, o bien una directiva de las
que veremos más adelante.
Campo de operandos. Indica cuales son los datos implicados en la operación. Puede haber 0, 1 ó 2; en el caso de que sean
dos al 1º se le llama destino y al 2º -separado por una coma- fuente.
mov ax, es:[di] --> ax destino
es:[di] origen
Campo de comentarios. Cuando en una línea hay un punto y coma (;) todo lo que sigue en la línea es un comentario que
realiza aclaraciones sobre lo que se está haciendo en ese programa, resulta de gran utilidad de cara a realizar futuras
modificaciones al mismo.
5.2. - CONSTANTES Y OPERADORES.
Las sentencias fuente -tanto instrucciones como directivas- pueden contener constantes y operadores.
5.2.1. - CONSTANTES.
Pueden ser binarias (ej. 10010b), decimales (ej. 34d), hexadecimales (ej. 0E0h) u octales (ej. 21o ó 21q); también las hay de
cadena (ej. 'pepe', "juan") e incluso con comillas dentro de comillas de distinto tipo (como 'hola,"amigo"'). En las
hexadecimales, si el primer dígito no es numérico hay que poner un 0. Sólo se puede poner el signo (-) en las decimales
(en las demás, calcúlese el complemento a dos). Por defecto, las numéricas están en base 10 si no se indica lo contrario
con una directiva (poco recomendable como se verá).
5.2.2. - OPERADORES ARITMÉTICOS.
Pueden emplearse libremente (+), (-), (*) y (/) -en este último caso la división es siempre entera-. Es válida, por ejemplo,
la siguiente línea en ensamblador (que se apoya en la directiva DW, que se verá más adelante, para reservar memoria para una
palabra de 16 bits):
dato DW 12*(numero+65)/7
También se admiten los operadores MOD (resto de la división) y SHL/SHR (desplazar a la izquierda/derecha cierto número de
bits). Obviamente, el ensamblador no codifica las instrucciones de desplazamiento (al aplicarse sobre datos constantes el
resultado se calcula en tiempo de ensamblaje):
dato DW (12 SHR 2) + 5
5.2.3. - OPERADORES LÓGICOS.
Pueden ser el AND, OR, XOR y NOT. Realizan las operaciones lógicas en las expresiones. Ej.:
MOV BL,(255 AND 128) XOR 128 ; BL = 0
5.2.4. - OPERADORES RELACIONALES.
Devuelven condiciones de cierto (0FFFFh ó 0FFh) o falso (0) evaluando una expresión. Pueden ser: EQ (igual), NE (no igual),
LT (menor que), GT (mayor que), LE (menor o igual que), GE (mayor o igual que). Ejemplo:
dato EQU 100 ; «dato» vale 100
MOV AL,dato GE 10 ; AL = 0FFh (cierto)
MOV AH,dato EQ 99 ; AH = 0 (falso)
5.2.5. - OPERADORES DE RETORNO DE VALORES.
* Operador SEG: devuelve el valor del segmento de la variable o etiqueta, sólo se puede emplear en programas de tipo EXE:
MOV AX,SEG tabla_datos
* Operador OFFSET: devuelve el desplazamiento de la variable o etiqueta en su segmento:
MOV AX,OFFSET variable
Si se desea obtener el offset de una variable respecto al grupo (directiva GROUP) de segmentos en que está definida
y no respecto al segmento concreto en que está definida:
MOV AX,OFFSET nombre_grupo:variable
también es válido:
MOV AX,OFFSET DS:variable
* Operador .TYPE: devuelve el modo de la expresión indicada en un byte. El bit 0 indica modo «relativo al código» y el
1 modo «relativo a datos», si ambos bits están inactivos significa modo absoluto. El bit 5 indica si la expresión es local
(0 si está definida externamente o indefinida); el bit 7 indica si la expresión contiene una referencia externa. El TASM
utiliza también el bit 3 para indicar algo que desconozco. Este operador es útil sobre todo en las macros para determinar
el tipo de los parámetros:
info .TYPE variable
* Operador TYPE: devuelve el tamaño (bytes) de la variable indicada. No válido en variables DUP:
kilos DW 76
MOV AX,TYPE kilos ; AX = 2
Tratándose de etiquetas -en lugar de variables- indica si es lejana o FAR (0FFFEh) o cercana o NEAR (0FFFFh).
* Operadores SIZE y LENGTH: devuelven el tamaño (en bytes) o el nº de elementos, respectivamente, de la variable indicada
(definida obligatoriamente con DUP):
matriz DW 100 DUP (12345)
MOV AX,SIZE matriz ; AX = 200
MOV BX,LENGTH matriz ; BX = 100
* Operadores MASK y WIDTH: informan de los campos de un registro de bits (véase RECORD).
5.2.6. - OPERADORES DE ATRIBUTOS.
* Operador PTR: redefine el atributo de tipo (BYTE, WORD, DWORD, QWORD, TBYTE) o el
de distancia (NEAR o FAR) de un operando de memoria. Por ejemplo, si se tiene una tabla definida de la siguiente manera:
tabla DW 10 DUP (0) ; 10 palabras a 0
Para colocar en AL el primer byte de la misma, la instrucción MOV AL,tabla es incorrecta, ya que tabla (una cadena
10 palabras) no cabe en el registro AL. Lo que desea el programador debe indicárselo en este caso explícitamente al
ensamblador de la siguiente manera:
MOV AL,BYTE PTR tabla
Trabajando con varios segmentos, PTR puede redefinir una etiqueta NEAR de uno de ellos para convertirla en FAR desde
el otro, con objeto de poder llamarla.
* Operadores CS:, DS:, ES: y SS: el ensamblador genera un prefijo de un byte que indica al microprocesador el segmento que
debe emplear para acceder a los datos en memoria. Por defecto, se supone DS para los registros BX, DI o SI (o sin registros
de base o índice) y SS para SP y BP. Si al acceder a un dato éste no se encuentra en el segmento por defecto, el ensamblador
añadirá el byte adicional de manera automática. Sin embargo, el programador puede forzar también esta circunstancia:
MOV AL,ES:variable
En el ejemplo, variable se supone ubicada en el segmento extra. Cuando se referencia una dirección fija hay que indicar
el segmento, ya que el ensamblador no conoce en qué segmento está la variable, es uno de los pocos casos en que debe
indicarse. Por ejemplo, la siguiente línea dará un error al ensamblar:
MOV AL,[0]
Para solucionarlo hay que indicar en qué segmento está el dato (incluso aunque éste sea DS):
MOV AL,DS:[0]
En este último ejemplo el ensamblador no generará el byte adicional ya que las instrucciones MOV operan por defecto
sobre DS (como casi todas), pero ha sido necesario indicar DS para que el ensamblador nos entienda. Sin embargo, en
el siguiente ejemplo no es necesario, ya que midato está declarado en el segmento de datos y el ensamblador lo sabe:
MOV AL,midato
Por lo general no es muy frecuente la necesidad de indicar explícitamente el segmento: al acceder a una variable
el ensamblador mira en qué segmento está declarada (véase la directiva SEGMENT) y según como estén asignados los
ASSUME, pondrá o no el prefijo adecuado según sea conveniente. Es responsabilidad exclusiva del programador inicializar
los registros de segmento al principio de los procedimientos para que el ASSUME no se quede en tinta mojada... sí
se emplean con bastante frecuencia, sin embargo, los prefijos CS en las rutinas que gestionan interrupciones (ya
que CS es el único registro de segmento que apunta en principio a las mismas, hasta que se cargue DS u otro).
* Operador SHORT: indica que la etiqueta referenciada, de tipo NEAR, puede alcanzarse con un salto corto (-128 a +127
posiciones) desde la actual situación del contador de programa. El ensamblador TASM, si se solicitan dos pasadas, coloca
automáticamente instrucciones SHORT allí donde es posible, para economizar memoria (el MASM no).
* Operador '$': indica la posición del contador de posiciones («Location Counter») utilizado por el ensamblador dentro
del segmento para llevar la cuenta de por dónde se llega ensamblando. Muy útil:
frase DB "simpático"
longitud EQU $-OFFSET frase
En el ejemplo, longitud tomará el valor 9.
* Operadores HIGH y LOW: devuelven la parte alta o baja, respectivamente (8 bits) de la expresión:
dato EQU 1025
MOV AL,LOW dato ; AL = 1
MOV AH,HIGH dato ; AH = 4
5.3. - PRINCIPALES DIRECTIVAS.
La sintaxis de una sentencia directiva es muy similar a la de una sentencia de instrucción:
[nombre] nombre_directiva [operandos] [comentario]
Sólo es obligatorio el campo «nombre_directiva»; los campos han de estar separados por al menos un espacio en blanco. La
sintaxis de «nombre» es análoga a la de la «etiqueta» de las líneas de instrucciones, aunque nunca se pone el sufijo «:».
El campo de comentario cumple también las mismas normas. A continuación se explican las directivas empleadas en los
programas ejemplo de este libro y alguna más, aunque falta alguna que otra y las explicadas no lo están en todos los
casos con profundidad.
5.3.1. - DIRECTIVAS DE DEFINICIÓN DE DATOS.
* DB (definir byte), DW (definir palabra), DD (definir doble palabra), DQ (definir cuádruple palabra), DT (definir 10
bytes): sirven para declarar las variables, asignándolas un valor inicial:
anno DW 1991
mes DB 12
numerazo DD 12345678h
texto DB "Hola",13,10
Se pueden definir números reales de simple precisión (4 bytes) con DD, de doble precisión (8 bytes) con DQ y «reales
temporales» (10 bytes) con DT; todos ellos con el formato empleado por el coprocesador. Para que el ensamblador
interprete el número como real ha de llevar el punto decimal:
temperatura DD 29.72
espanoles91 DQ 38.9E6
Con el operando DUP pueden definirse estructuras repetitivas. Por ejemplo, para asignar 100 bytes a cero y 25
palabras de contenido indefinido (no importa lo que el ensamblador asigne):
ceros DB 100 DUP (0)
basura DW 25 DUP (?)
Se admiten también los anidamientos. El siguiente ejemplo crea una tabla de bytes donde se repite 50 veces la
secuencia 1,2,3,7,7:
tabla DB 50 DUP (1, 2, 3, 2 DUP (7))
5.3.2. - DIRECTIVAS DE DEFINICIÓN DE SÍMBOLOS.
* EQU (EQUivalence): Asigna el valor de una expresión a un nombre simbólico fijo:
olimpiadas EQU 1992
Donde olimpiadas ya no podrá cambiar de valor en todo el programa. Se trata de un operador muy flexible. Es
válido hacer:
edad EQU [BX+DI+8]
MOV AX,edad
* = (signo '='): asigna el valor de la expresión a un nombre simbólico variable: Análogo al anterior pero con posibilidad
de cambiar en el futuro. Muy usada en macros (sobre todo con REPT).
num = 19
num = pepe + 1
dato = [BX+3]
dato = ES:[BP+1]
5.3.3. - DIRECTIVAS DE CONTROL DEL ENSAMBLADOR.
* ORG (ORiGin): pone el contador de posiciones del ensamblador, que indica el offset donde se deposita la instrucción o
dato, donde se indique. En los programas COM (que se cargan en memoria con un OFFSET 100h) es necesario colocar al
principio un ORG 100h, y un ORG 0 en los controladores de dispositivo (aunque si se omite se asume de hecho un ORG 0).
* END [expresión]: indica el final del fichero fuente. Si se incluye, expresión indica el punto donde arranca el programa.
Puede omitirse en los programas EXE si éstos constan de un sólo módulo. En los COM es preciso indicarla y, además, la
expresión -realmente una etiqueta- debe estar inmediatamente después del ORG 100h.
* .286, .386 Y .8087 obligan al ensamblador a reconocer instrucciones específicas del 286, el 386 y del 8087. También
debe ponerse el «.» inicial. Con .8086 se fuerza a que de nuevo sólo se reconozcan instrucciones del 8086 (modo por
defecto). La directiva .386 puede ser colocada dentro de un segmento (entre las directivas SEGMENT/ENDS) con el ensamblador
TASM, lo que permite emplear instrucciones de 386 con segmentos de 16 bits; alternativamente se puede ubicar fuera de los
segmentos (obligatorio en MASM) y definir éstos explícitamente como de 16 bits con USE16.
* EVEN: fuerza el contador de posiciones a una posición par, intercalando un byte con la instrucción NOP si es preciso.
En buses de 16 ó más bits (8086 y superiores, no en 8088) es dos veces más rápido el acceso a palabras en posición par:
EVEN
dato_rapido DW 0
* .RADIX n: cambia la base de numeración por defecto. Bastante desaconsejable dada la notación elegida para indicar las
bases por parte de IBM/Microsoft (si se cambia la base por defecto a 16, ¡los números no pueden acabar en 'd' ya que se
confundirían con el sufijo de decimal!: lo ideal sería emplear un prefijo y no un sufijo, que a menudo obliga además a
iniciar los números por 0 para distinguirlos de las etiquetas).
5.3.4. - DIRECTIVAS DE DEFINICIÓN DE SEGMENTOS Y PROCEDIMIENTOS.
* SEGMENT-ENDS: SEGMENT indica el comienzo de un segmento (código, datos, pila, etc.) y ENDS su final. El programa más
simple, de tipo COM, necesita la declaración de un segmento (común para datos, código y pila). Junto a SEGMENT puede
aparecer, opcionalmente, el tipo de alineamiento, la combinación, el uso y la clase:
nombre SEGMENT [alineamiento] [combinación] [uso] ['clase']
. . . .
nombre ENDS
Se pueden definir unos segmentos dentro de otros (el ensamblador los ubicará unos tras otros). El alineamiento
puede ser BYTE (ninguno), WORD (el segmento comienza en posición par), DWORD (comienza en posición múltiplo de 4),
PARA (comienza en una dirección múltiplo de 16, opción por defecto) y PAGE (comienza en dirección múltiplo de 256).
La combinación puede ser:
- (No indicada): los segmentos se colocan unos tras otros físicamente, pero son lógicamente independientes: cada
uno tiene su propia base y sus propios offsets relativos.
- PUBLIC: usado especialmente cuando se trabaja con segmentos definidos en varios ficheros que se ensamblan por
separado o se compilan con otros lenguajes, por ello debe declararse un nombre entre comillas simples -'clase'-
para ayudar al linkador. Todos los segmentos PUBLIC de igual nombre y clase tienen una base común y son colocados
adyacentemente unos tras otros, siendo el offset relativo al primer segmento cargado.
- COMMON: similar, aunque ahora los segmentos de igual nombre y clase se solapan. Por ello, las variables
declaradas han de serlo en el mismo orden y tamaño.
- AT: asocia un segmento a una posición de memoria fija, no para ensamblar sino para declarar variables
(inicializadas siempre con '?') de cara a acceder con comodidad a zonas de ROM, vectores de interrupción, etc. Ejemplo:
vars_bios SEGMENT AT 40h
p_serie0 DW ?
vars_bios ENDS
De esta manera, la dirección del primer puerto serie puede obtenerse de esta manera (por ejemplo):
MOV AX,variables_bios ; segmento
MOV ES,AX ; inicializar ES
MOV AX,ES:p_serie0
- STACK: segmento de pila, debe existir uno en los programas de tipo EXE; además el Linkador de Borland (TLINK 4.0)
exige obligatoriamente que la clase de éste sea también 'STACK', con el LINK de Microsoft no siempre es necesario
indicar la clase del segmento de pila. Similar, por lo demás, a PUBLIC.
- MEMORY: segmento que el linkador ubicará al final de todos los demás, lo que permitiría saber dónde acaba el
programa. Si se definen varios segmentos de este tipo el ensamblador acepta el primero y trata a los demás como
COMMON. Téngase en cuenta que el linkador no soporta esta característica, por lo que emplear MEMORY es equivalente
a todos los efectos a utilizar COMMON. Olvídate de MEMORY.
El uso indica si el segmento es de 16 bits o de 32; al emplear la directiva .386 se asumen por defecto segmentos de
32 bits por lo que es necesario declarar USE16 para conseguir que los segmentos sean interpretados como de 16 bits
por el linkador, lo que permite emplear algunas instrucciones del 386 en el modo real del microprocesador y bajo el
sistema operativo DOS.
Por último, 'clase' es un nombre opcional que empleará el linkador para encadenar los módulos, siendo conveniente
nombrar la clase del segmento de pila con 'STACK'.
* ASSUME (Suponer): Indica al ensamblador el registro de segmento que se va a utilizar para direccionar cada segmento
dentro del módulo. Esta instrucción va normalmente inmediatamente después del SEGMENT. El programa más sencillo necesita
que se «suponga» CS como mínimo para el segmento de código, de lo contrario el ensamblador empezará a protestar un montón
al no saber que registro de segmento asociar al código generado. También conviene hacer un assume del registro de segmento
DS hacia el segmento de datos, incluso en el caso de que éste sea el mismo que el de código: si no, el ensamblador colocará
un byte de prefijo adicional en todos los accesos a memoria para forzar que éstos sean sobre CS. Se puede indicar ASSUME
NOTHING para cancelar un ASSUME anterior. También se puede indicar el nombre de un grupo o emplear «SEG variable» o «SEG
etiqueta» en vez de nombre_segmento:
ASSUME reg_segmento:nombre_segmento[,...]
* PROC-ENDP permite dar nombre a una subrutina, marcando con claridad su inicio y su fin.
Aunque es redundante, es muy recomendable para estructurar los programas.
cls PROC
...
cls ENDP
El atributo FAR que aparece en ocasiones junto a PROC indica que es un procedimiento lejano y las instrucciones
RET en su interior se ensamblan como RETF (los CALL hacia él serán, además, de 32 bits). Observar que la etiqueta
nunca termina con dos puntos.
5.3.5. - DIRECTIVAS DE REFERENCIAS EXTERNAS.
* PUBLIC: permite hacer visibles al exterior (otros ficheros objeto resultantes de otros listados en ensamblador u otro
lenguaje) los símbolos -variables y procedimientos- indicados. Necesario para programación modular e interfaces con
lenguajes de alto nivel. Por ejemplo:
PUBLIC proc1, var_x
proc1 PROC FAR
...
proc1 ENDP
var_x DW 0
Declara la variable var_x y el procedimiento proc1 como accesibles desde el exterior por medio de la directiva EXTRN.
* EXTRN: Permite acceder a símbolos definidos en otro fichero objeto (resultante de otro ensamblaje o de una compilación
de un lenguaje de alto nivel); es necesario también indicar el tipo del dato o procedimiento (BYTE, WORD o DWORD; NEAR o
FAR; se emplea además ABS para las constantes numéricas):
EXTRN proc1:FAR, var_x:WORD
En el ejemplo se accede a los símbolos externos proc1 y var_x (ver ejemplos de PUBLIC) y a continuación sería posible
hacer un CALL proc1 o un MOV CX,var_x. Si la directiva EXTRN se coloca dentro de un segmento, se supone el símbolo
dentro del mismo. Si el símbolo está en otro segmento, debe colocarse EXTRN fuera de todos los segmentos indicando
explícitamente el prefijo del registro de segmento (o bien hacer el ASSUME apropiado) al referenciarlo. Evidentemente,
al final, al linkar habrá que enlazar este módulo con el que define los elementos externos.
* INCLUDE nombre_fichero: Añade al fichero fuente en proceso de ensamblaje el fichero indicado, en el punto en que aparece
el INCLUDE. Es exactamente lo mismo que mezclar ambos ficheros con un editor de texto. Ahorra trabajo en fragmentos de
código que se repiten en varios programas (como quizá una librería de macros). No se recomiendan INCLUDE's anidados.
5.3.6. - DIRECTIVAS DE DEFINICIÓN DE BLOQUES.
* NAME nombre_modulo_objeto: indica el nombre del módulo objeto. Si no se incluye NAME, se tomará de la directiva TITLE o,
en su defecto, del nombre del propio fichero fuente.
* GROUP segmento1, segmento2,... permite agrupar dos o más segmentos lógicos en uno sólo de no más de 64 Kb totales (ojo:
el ensamblador no comprueba este extremo, aunque sí el enlazador). Ejemplo:
superseg GROUP datos, codigo, pila
codigo SEGMENT
...
codigo ENDS
datos SEGMENT
dato DW 1234
datos ENDS
pila SEGMENT STACK 'STACK'
DB 128 DUP (?)
pila ENDS
Cuando se accede a un dato definido en algún segmento de un grupo y se emplea el operador OFFSET es preciso indicar
el nombre del grupo como prefijo, de lo contrario el ensamblador no generará el desplazamiento correcto ¡ni emitirá
errores!:
MOV AX,dato ; ¡incorrecto!
MOV AX,supersegmento:dato ; correcto
La ventaja de agrupar segmentos es poder crear programas COM y SYS que contengan varios segmentos. En todo caso,
téngase en cuenta aún en ese caso que no pueden emplearse todas las características de la programación con segmentos
(por ejemplo, no se puede utilizar la directiva SEG ni debe existir segmento de pila).
* LABEL: Permite referenciar un símbolo con otro nombre, siendo factible redefinir el tipo. La sintaxis es: nombre LABEL
tipo (tipo = BYTE, WORD, DWORD, NEAR o FAR). Ejemplo:
palabra LABEL WORD
byte_bajo DB 0
byte_alto DB 0
En el ejemplo, con MOV AX,palabra se accederá a ambos bytes a la vez (el empleo de MOV AX,byte_bajo daría error: no
se puede cargar un sólo byte en un registro de 16 bits y el ensamblador no supone que realmente pretendíamos tomar
dos bytes consecutivos de la memoria).
* STRUC - ENDS: permite definir registros al estilo de los lenguajes de alto nivel, para acceder de una manera más elegante
a los campos de una información con cierta estructura. Estos campos pueden componerse de cualquiera de los tipos de datos
simples (DB, DW, DD, DQ, DT) y pueden ser modificables o no en función de si son simples o múltiples, respectivamente:
alumno STRUC
mote DB '0123456789' ; modificable
edadaltura DB 20,175 ; no modificable
peso DB 0 ; modificable
otros DB 10 DUP(0) ; no modificable
telefono DD ? ; modificable
alumno ENDS
La anterior definición de estructura no lleva implícita la reserva de memoria necesaria, la cual ha de hacerse
expresamente utilizando los ángulos '<' y '>':
felipe alumno <'Gordinflas',,101,,251244>
En el ejemplo se definen los campos modificables (los únicos definibles) dejando sin definir (comas consecutivas)
los no modificables, creándose la estructura 'felipe' que ocupa 27 bytes. Las cadenas de caracteres son rellenadas
con espacios en blanco al final si no alcanzan el tamaño máximo de la declaración. El TASM es más flexible y permite
definir también el primer elemento de los campos múltiples sin dar error. Tras crear la estructura, es posible
acceder a sus elementos utilizando un (.) para separar el nombre del campo:
MOV AX,OFFSET felipe.telefono
LEA BX,felipe
MOV CL,[BX].peso ; equivale a [BX+12]
* RECORD: similar a STRUC pero operando con campos de bits. Permite definir una estructura determinada de byte o palabra
para operar con comodidad. Sintaxis:
nombre RECORD nombre_de_campo:tamaño[=valor],...
Donde nombre permitirá referenciar la estructura en el futuro, nombre_de_campo identifica los distintos campos, a
los que se asigna un tamaño (en bits) y opcionalmente un valor por defecto.
registro RECORD a:2=3, b:4=5, c:1
La estructura registro totaliza 7 bits, por lo que ocupa un byte. Está dividida en tres campos que ocupan los 7 bits
menos significativos del byte: el campo A ocupa los bits 6 y 5, el B los bits del byte: el campo A ocupa los bi1 al
4 y el C el bit 0:
6 5 4 3 2 1 0
1 1 0 1 0 1 ?
La reserva de memoria se realiza, por ejemplo, de la siguiente manera:
reg1 registro <2,,1>
Quedando reg1 con el valor binario 1001011 (el campo B permanece inalterado y el A y C toman los valores indicados).
Ejemplos de operaciones soportadas:
MOV AL, A ; AL = 5 (desplazamiento del bit
; menos significativo de A)
MOV AL, MASK A ; AL = 01100000b (máscara de A)
MOV AL, WIDTH A ; AL = 2 (anchura de A)
5.3.7. - DIRECTIVAS CONDICIONALES.
Se emplean para que el ensamblador evalúe unas condiciones y, según ellas, ensamble o no ciertas zonas de código. Es
frecuente, por ejemplo, de cara a generar código para varios ordenadores: pueden existir ciertos símbolos definidos
que indiquen en un momento dado si hay que ensamblar ciertas zonas del listado o no de manera condicional, según la
máquina. En los fragmentos en ensamblador del código que generan los compiladores también aparecen con frecuencia
(para actuar de manera diferente, por ejemplo, según el modelo de memoria). Es interesante también la posibilidad de
definir un símbolo que indique que el programa está en fase de pruebas y ensamblar código adicional en ese caso con
objeto de depurarlo. Sintaxis:
IFxxx [símbolo/exp./arg.] ; xxx es la condición
...
ELSE ; el ELSE es opcional
...
ENDIF
IF expresion (expresión distinta de cero)
IFE expresión (expresión igual a cero)
IF1 (pasada 1 del ensamblador)
IF2 (pasada 2 del ensamblador)
IFDEF símbolo (símbolo definido o declarado como externo)
IFNDEF símbolo (símbolo ni definido ni declarado como externo)
IFB <argumento> (argumento en blanco en macros -incluir '<' y '>'-)
IFNB <argumento> (lo contrario, también es obligado poner '<' y '>')
IFIDN <arg1>, <arg2> (arg1 idéntico a arg2, requiere '<' y '>')
IFDIF <arg1>, <arg2> (arg1 distinto de arg2, requiere '<' y '>')
5.3.8. - DIRECTIVAS DE LISTADO.
* PAGE num_lineas, num_columnas: Formatea el listado de salida; por defecto son 66 líneas por página (modificable entre 10
y 255) y 80 columnas (seleccionable de 60 a 132). PAGE salta de página e incrementa su número. «PAGE +» indica capítulo
nuevo (y se incrementa el número).
* TITLE título: indica el título que aparece en la 1ª línea de cada página (máximo 60 caracteres).
* SUBTTL subtítulo: Ídem con el subtítulo (máx. 60 caracteres).
* .LALL: Listar las macros y sus expansiones.
* .SALL: No listar las macros ni sus expansiones.
* .XALL: Listar sólo las macros que generan código objeto.
* .XCREF: Suprimir listado de referencias cruzadas (listado alfabético de símbolos junto al nº de línea en que son definidos
y referenciados, de cara a facilitar la depuración).
* .CREF: Restaurar listado de referencias cruzadas.
* .XLIST: Suprimir el listado ensamblador desde ese punto.
* .LIST: Restaurar de nuevo la salida de listado ensamblador.
* COMMENT delimitador comentario delimitador: Define un comentario que puede incluso ocupar varias líneas, el delimitador
(primer carácter no blanco ni tabulador que sigue al COMMENT) indica el inicio e indicará más tarde el final del comentario.
¡No olvidar cerrar el comentario!.
* %OUT mensaje: escribe en la consola el mensaje indicado durante la fase de ensamblaje y al llegar a ese punto del listado,
excepto cuando el listado es por pantalla y no en fichero.
* .LFCOND: Listar los bloques de código asociados a una condición falsa (IF).
* .SFCOND: suprimir dicho listado.
* .TFCOND: Invertir el modo vigente de listado de los bloques asociados a una condición falsa.
5.4. - MACROS.
Cuando un conjunto de instrucciones en ensamblador aparecen frecuentemente repetidas a lo largo de un listado, es conveniente
agruparlas bajo un nombre simbólico que las sustituirá en aquellos puntos donde aparezcan. Esta es la misión de las macros;
por el hecho de soportarlas el ensamblador eleva su categoría a la de macroensamblador, al ser las macros una herramienta
muy cotizada por los programadores.
No conviene confundir las macros con subrutinas: es estas últimas, el conjunto de instrucciones aparece una sola vez en todo
el programa y luego se invoca con CALL. Sin embargo, cada vez que se referencia a una macro, el código que ésta representa
se expande en el programa definitivo, duplicándose tantas veces como se use la macro. Por ello, aquellas tareas que puedan
ser realizadas con subrutinas siempre será más conveniente realizarlas con las mismas, con objeto de economizar memoria. Es
cierto que las macros son algo más rápidas que las subrutinas (se ahorra un CALL y un RET) pero la diferencia es tan mínima
que en la práctica es despreciable en el 99,99% de los casos. Por ello, es absurdo e irracional realizar ciertas tareas con
macros que pueden ser desarrolladas mucho más eficientemente con subrutinas: es una pena que en muchos manuales de ensamblador
aún se hable de macros para realizar operaciones sobre cadenas de caracteres, que generarían programas gigantescos con menos
de un 1% de velocidad adicional.
5.4.1. - DEFINICIÓN Y BORRADO DE LAS MACROS.
La macro se define por medio de la directiva MACRO. Es necesario definir la macro antes de utilizarla. Una macro puede llamar
a otra. Con frecuencia, las macros se colocan juntas en un fichero independiente y luego se mezclan en el programa principal
con la directiva INCLUDE:
IF1
INCLUDE fichero.ext
ENDIF
La sentencia IF1 asegura que el ensamblador lea el fichero fuente de las macros sólo en la primera pasada, para acelerar el
ensamblaje y evitar que aparezcan en el listado (generado en la segunda fase). Conviene hacer hincapié en que la definición
de la macro no consume memoria, por lo que en la práctica es indiferente declarar cientos que ninguna macro:
nombre_simbólico MACRO [parámetros]
...
... ; instrucciones de la macro
ENDM
El nombre simbólico es el que permitirá en adelante hacer referencia a la macro, y se construye casi con las mismas reglas
que los nombres de las variables y demás símbolos. La macro puede contener parámetros de manera opcional. A continuación
vienen las instrucciones que engloba y, finalmente, la directiva ENDM señala el final de la macro. No se debe repetir el
nombre simbólico junto a la directiva ENDM, ello provocaría un error un tanto curioso y extraño por parte del ensamblador
(algo así como «Fin del fichero fuente inesperado, falta directiva END»), al menos con MASM 5.0 y TASM 2.0.
En realidad, y a diferencia de lo que sucede con los demás símbolos, el nombre de una macro puede coincidir con el de una
instrucción máquina o una directiva del ensamblador: a partir de ese momento, la instrucción o directiva machacada pierde
su significado original. El ensamblador dará además un aviso de advertencia si se emplea una instrucción o directiva como
nombre de macro, aunque tolerará la operación. Normalmente se las asignará nombres normales, como a las variables. Sin
embargo, si alguna vez se redefiniera una instrucción máquina o directiva, para restaurar el significado original del
símbolo, la macro puede ser borrada -o simplemente porque ya no va a ser usada a partir de cierto punto del listado, y
así ya no consumirá espacio en las tablas de macros que mantiene en memoria el ensamblador al ensamblar-. No es necesario
borrar las macros antes de redefinirlas. Para borrarlas, la sintaxis es la siguiente:
PURGE nombre_simbólico[,nombre_simbólico,...]
5.4.2. - EJEMPLO DE UNA MACRO SENCILLA.
Desde el 286 existe una instrucción muy cómoda que introduce en la pila 8 registros, y otra que los saca (PUSHA y POPA).
Quien esté acostumbrado a emplearlas, puede crear unas macros que simulen estas instrucciones en los 8086:
SUPERPUSH MACRO
PUSH AX
PUSH CX
PUSH DX
PUSH BX
PUSH SP
PUSH BP
PUSH SI
PUSH DI
ENDM
La creación de SUPERPOP es análoga, sacando los registros en orden inverso. El orden elegido no es por capricho y se
corresponde con el de la instrucción PUSHA original, para compatibilizar. A partir de la definición de esta macro,
tenemos a nuestra disposición una nueva instrucción máquina (SUPERPUSH) que puede ser usada con libertad dentro de los programas.
5.4.3. - PARÁMETROS FORMALES Y PARÁMETROS ACTUALES.
Para quien no haya tenido relación previa con algún lenguaje estructurado de alto nivel, haré un breve comentario acerca
de lo que son los parámetros formales y actuales en una macro, similar aquí a los procedimientos de los lenguajes de alto nivel.
Cuando se llama a una macro se le pueden pasar opcionalmente un cierto número de parámetros de cierto tipo. Estos
parámetros se denominan parámetros actuales. En la definición de la macro, dichos parámetros aparecen asociados a
ciertos nombres arbitrarios, cuya única misión es permitir distinguir unos parámetros de otros e indicar en qué orden
son entregados: son los parámetros formales. Cuando el ensamblador expanda la macro al ensamblar, los parámetros
formales serán sustituidos por sus correspondientes parámetros actuales. Considerar el siguiente ejemplo:
SUMAR MACRO a,b,total
PUSH AX
MOV AX,a
ADD AX,b
MOV total,AX
POP AX
ENDM
....
SUMAR positivos, negativos, total
En el ejemplo, «a», «b» y «total» son los parámetros formales y «positivos», «negativos» y «total» son los parámetros
actuales. Tanto «a» como «b» pueden ser variables, etiquetas, etc. en otro punto del programa; sin embargo, dentro de
la macro, se comportan de manera independiente. El parámetro formal «total» ha coincidido en el ejemplo y por casualidad
con su correspondiente actual. El código que genera el ensamblador al expandir la macro será el siguiente:
PUSH AX
MOV AX,positivos
ADD AX,negativos
MOV total,AX
POP AX
Las instrucciones PUSH y POP sirven para no alterar el valor de AX y conseguir que la macro se comporte como una caja
negra; no es necesario que esto sea así pero es una buena costumbre de programación para evitar que los programas hagan
cosas raras. En general, las macros de este tipo no deberían alterar los registros y, si los cambian, hay que tener muy
claro cuáles.
Si se indican más parámetros de los que una macro necesita, se ignorarán los restantes. En cambio, si faltan, el MASM
asumirá que son nulos (0) y dará un mensaje de advertencia, el TASM es algo más rígido y podría dar un error. En general,
se trata de situaciones atípicas que deben ser evitadas.
También puede darse el caso de que no sea posible expandir la macro. En el ejemplo, no hubiera sido posible ejecutar
SUMAR AX,BX,DL porque DL es de 8 bits y la instrucción MOV DL,AX sería ilegal.
5.4.4. - ETIQUETAS DENTRO DE MACROS. VARIABLES LOCALES.
Son necesarias normalmente para los saltos condicionales que contengan las macros más complejas. Si se pone una etiqueta
a donde saltar, la macro sólo podría ser empleada una vez en todo el programa para evitar que dicha etiqueta aparezca
duplicada. La solución está en emplear la directiva LOCAL que ha de ir colocada justo después de la directiva MACRO:
MINIMO MACRO dato1, dato2, resultado
LOCAL ya_esta
MOV AX,dato1
CMP AX,dato2 ; ¿es dato1 el menor?
JB ya_esta ; sí
MOV AX,dato2 ; no, es dato2
ya_esta: MOV resultado,AX
ENDM
En el ejemplo, al invocar la macro dos veces el ensamblador no generará la etiqueta «ya_esta» sino las etiquetas
??0000, ??0001, ... y así sucesivamente. La directiva LOCAL no sólo es útil para los saltos condicionales en las
macros, también permite declarar variables internas a los mismos. Se puede indicar un número casi indefinido de
etiquetas con la directiva LOCAL, separándolas por comas.
5.4.5. - OPERADORES DE MACROS.
* Operador ;;
Indica que lo que viene a continuación es un comentario que no debe aparecer al expansionar la macro. Cuando
al ensamblar se genera un listado del programa, las macros suelen aparecer expandidas en los puntos en que se
invocan; sin embargo sólo aparecerán los comentarios normales que comiencen por (;). Los comentarios relacionados
con el funcionamiento interno de la macro deberían ir con (;;), los relativos al uso y sintaxis de la misma con
(;). Esto es además conveniente porque durante el ensamblaje son mantenidos en memoria los comentarios de macros
(no los del resto del programa) que comienzan por (;), y no conviene desperdiciar memoria...
* Operador &
Utilizado para concatenar texto o símbolos. Es necesario para lograr que el ensamblador sustituya un parámetro
dentro de una cadena de caracteres o como parte de un símbolo:
SALUDO MACRO c
MOV AL,"&c"
etiqueta&c: CALL imprimir
ENDM
Al ejecutar SALUDO A se producirá la siguiente expansión:
MOV AL,"A"
etiquetaA: CALL imprimir
Si no se hubiera colocado el & se hubiera expandido como MOV AL,"c"
Cuando se utilizan estructuras repetitivas REPT, IRP o IRPC (que se verán más adelante) existe un problema
adicional al intentar crear etiquetas, ya que el ensamblador se come un & al hacer la primera sustitución,
generando la misma etiqueta a menos que se duplique el operador &:
MEMORIA MACRO x
IRP i, <1, 2>
x&i DB i
ENDM
ENDM
Si se invoca MEMORIA ET se produce el error de "etiqueta ETi repetida", que se puede salvar añadiendo tantos
'&' como niveles de anidamiento halla en las estructuras repetitivas empleadas, como se ejemplifica a continuación:
MEMORIA MACRO x
IRP i, <1, 2>
x&&i DB i
ENDM
ENDM
Lo que con MEMORIA ET generará correctamente las líneas:
ET1 DB 1
ET2 DB 2
* Operador ! o <>
Empleado para indicar que el carácter que viene a continuación debe ser interpretado literalmente y no como un
símbolo. Por ello, !; es equivalente a <;>.
* Operador %
Convierte la expresión que le sigue -generalmente un símbolo- a un número; la expresión debe ser una constante
(no relocalizable). Sólo se emplea en los argumentos de macros. Dada la macro siguiente:
PSUM MACRO mensaje, suma
%OUT * mensaje, suma *
ENDM
(Evidentemente, el % que precede a OUT forma parte de la directiva y no se trata del % operador que estamos tratando)
Supuesta la existencia de estos símbolos:
SIM1 EQU 120
SIM2 EQU 500
Invocando la macro con las siguientes condiciones:
PSUM < SIM1 + SIM2 = >, (SIM1+SIM2)
Se produce la siguiente expansión:
%OUT * SIM1 + SIM2 = (SIM1+SIM2) *
Sin embargo, invocando la macro de la siguiente manera (con %):
PSUM < SIM1 + SIM2 = >, %(SIM1+SIM2)
Se produce la expansión deseada:
%OUT * SIM1 + SIM2 = 620 *
5.4.6. - DIRECTIVAS ÚTILES PARA MACROS.
Estas directivas pueden ser empleadas también sin las macros, aumentando la comodidad de la programación, aunque
abundan especialmente dentro de las macros.
* REPT veces ... ENDM (Repeat)
Permite repetir cierto número de veces una secuencia de instrucciones. El bloque de instrucciones se delimita
con ENDM (no confundirlo con el final de una macro). Por ejemplo:
REPT 2
OUT DX,AL
ENDM
Esta secuencia se transformará, al ensamblar, en lo siguiente:
OUT DX,AL
OUT DX,AL
Empleando símbolos definidos con (=) y apoyándose además en las macros se puede llegar a crear pseudo-instrucciones
muy potentes:
SUCESION MACRO n
num = 0
REPT n
DB num
num = num + 1
ENDM ; fin de REPT
ENDM ; fin de macro
La sentencia SUCESION 3 provocará la siguiente expansión:
DB 0
DB 1
DB 2
* IRP simbolo_control, <arg1, arg2, ..., arg_n> ... ENDM (Indefinite repeat)
Es relativamente similar a la instrucción FOR de los lenguajes de alto nivel. Los ángulos (<) y (>) son
obligatorios. El símbolo de control va tomando sucesivamente los valores (no necesariamente numéricos) arg1,
arg2, ... y recorre en cada pasada todo el bloque de instrucciones hasta alcanzar el ENDM (no confundirlo con
fin de macro) sustituyendo simbolo_control por esos valores en todos los lugares en que aparece:
IRP i, <1,2,3>
DB 0, i, i*i
ENDM
Al expansionarse, este conjunto de instrucciones se convierte en lo siguiente:
DB 0, 1, 1
DB 0, 2, 4
DB 0, 3, 9
Nota: Todo lo encerrado entre los ángulos se considera un único parámetro. Un (;) dentro de los ángulos no se
interpreta como el inicio de un comentario sino como un elemento más. Por otra parte, al emplear macros anidadas, deben indicarse tantos símbolos angulares '<' y '>' consecutivos como niveles de anidamiento existan.
Lógicamente, dentro de una macro también resulta bastante útil la estructura IRP:
TETRAOUT MACRO p1, p2, p3, p4, valor
PUSH AX
PUSH DX
MOV AL,valor
IRP cn, <p1, p2, p3, p4>
MOV DX, cn
OUT DX, AL
ENDM ; fin de IRP
POP DX
POP AX
ENDM ; fin de macro
Al ejecutar TETRAOUT 318h, 1C9h, 2D1h, 1A4h, 17 se obtendrá:
PUSH AX
PUSH DX
MOV AL, 17
MOV DX, 318h
OUT DX, AL
MOV DX, 1C9h
OUT DX, AL
MOV DX, 2D1h
OUT DX, AL
MOV DX, 1A4h
OUT DX,AL
POP DX
POP AX
Cuando se pasan listas como parámetros hay que encerrarlas entre '<' y '>' al llamar, para no confundirlas con
elementos independientes. Por ejemplo, supuesta la macro INCD:
INCD MACRO lista, p
IRP i, <lista>
INC i
ENDM ; fin de IRP
DEC p
ENDM ; fin de macro
Se comprende la necesidad de utilizar los ángulos:
INCD AX, BX, CX, DX se expandirá:
INC AX
DEC BX ; CX y DX se ignoran (4 parámetros)
INCD <AX, BX, CX>, DX se expandirá:
INC AX
INC BX
INC CX
DEC DX ; (2 parámetros)
* IRPC simbolo_control, <c1c2 ... cn> ... ENDM (Indefinite repeat character)
Esta directiva es similar a la anterior, con una salvedad: los elementos situados entre los ángulos (<) y (>)
-ahora opcionales, por cierto- son caracteres ASCII y no van separados por comas:
IRPC i, <813>
DB i
ENDM
El bloque anterior generará al expandirse:
DB 8
DB 1
DB 3
Ejemplo de utilización dentro de una macro (en combinación con el operador &):
INICIALIZA MACRO a, b, c, d
IRPC iter, <&a&b&c&d>
DB iter
ENDM ; fin de IRPC
ENDM ; fin de macro
Al ejecutar INICIALIZA 7, 1, 4, 0 se produce la siguiente expansión:
DB 7
DB 1
DB 4
DB 0
* EXITM
Sirve para abortar la ejecución de un bloque MACRO, REPT, IRP ó IRPC. Normalmente se utiliza apoyándose en una
directiva condicional (IF...ELSE...ENDIF). Al salir del bloque, se pasa al nivel inmediatamente superior (que
puede ser otro bloque de estos). Como ejemplo, la siguiente macro reserva n bytes de memoria a cero hasta un
máximo de 100, colocando un byte 255 al final del bloque reservado:
MALLOC MACRO n
maximo=100
REPT n
IF maximo EQ 0 ; ¿ya van 100?
EXITM ; abandonar REPT
ENDIF
maximo = maximo - 1
DB 0 ; reservar byte
ENDM
DB 255 ; byte de fin de bloque
ENDM
5.4.7. - MACROS AVANZADAS CON NUMERO VARIABLE DE PARÁMETROS.
Como se vio al estudiar la directiva IF, existe la posibilidad de chequear condicionalmente la presencia de un
parámetro por medio de IFNB, o su ausencia con IFB. Uniendo esto a la potencia de IRP es posible crear macros
extraordinariamente versátiles. Como ejemplo, valga la siguiente macro, destinada a introducir en la pila un número
variable de parámetros (hasta 10): es especialmente útil en los programas que gestionan interrupciones:
XPUSH MACRO R1,R2,R3,R4,R5,R6,R7,R8,R9,R10
IRP reg, <R1,R2,R3,R4,R5,R6,R7,R8,R9,R10>
IFNB <reg>
PUSH reg
ENDIF
ENDM ; fin de IRP
ENDM ; fin de XPUSH
Por ejemplo, la instrucción:
XPUSH AX,BX,DS,ES,VAR1
Se expandirá en:
PUSH AX
PUSH AX
PUSH DS
PUSH ES
PUSH VAR1
El ejemplo anterior es ilustrativo del mecanismo de comprobación de presencia de parámetros. Sin embargo, este
ejemplo puede ser optimizado notablemente empleando una lista como único parámetro:
XPUSH MACRO lista
IRP i, <lista>
PUSH i
ENDM
ENDM
XPOP MACRO lista
IRP i, <lista>
POP i
ENDM
ENDM
La ventaja es el número indefinido de parámetros soportados (no sólo 10). Un ejemplo de uso puede ser el siguiente:
XPUSH <AX, BX, CX>
XPOP <CX, BX, AX>
Que al expandirse queda:
PUSH AX
PUSH BX
PUSH CX
POP CX
POP BX
POP AX
5.5. - PROGRAMACIÓN MODULAR Y PASO DE PARÁMETROS.
Aunque lo que viene a continuación no es indispensable para programar en ensamblador, sí es conveniente leerlo en
2 ó 3 minutos para observar ciertas reglas muy sencillas que ayudarán a hacer programas seguros y eficientes. Sin
embargo, personalmente considero que cada uno es muy libre de hacer lo que desee; por otra parte, en muchos casos
no se pueden cumplir los principios de la programación elegante -especialmente en ensamblador- por lo que detesto
aquellos profesionales de la informática que se entrometen con la manera de programar de sus colegas o alumnos,
obligándolos a hacer las cosas a su gusto.
La programación modular consiste en dividir los problemas más complejos en módulos separados con unas ciertas
interdependencias, lo que reduce el tiempo de programación y aumenta la fiabilidad del código. Se pueden implementar
en ensamblador con las directivas PROC y ENDP que, aunque no generan código son bastante útiles para dejar bien claro
dónde empieza y acaba un módulo. Reglas para la buena programación:
- Dividir los problemas en módulos pequeños relacionados sólo por un conjunto de parámetros de entrada y salida.
- Una sola entrada y salida en cada módulo: un módulo sólo debe llamar al inicio de otro (con CALL) y éste debe
retornar al final con un único RET, no debiendo existir más puntos de salida y no siendo recomendable alterar la
dirección de retorno.
- Excepto en los puntos en que la velocidad o la memoria son críticas (la experiencia demuestra que son menos del
1%) debe codificarse el programa con claridad, si es preciso perdiendo eficiencia. Ese 1% documentarlo profusamente
como se haría para que lo lea otra persona.
- Los módulos han de ser «cajas negras» y no deben modificar el entorno exterior. Esto significa que no deben
actuar sobre variables globales ni modificar los registros (excepto aquellos registros y variables en que devuelven
los resultados, lo que debe documentarse claramente al principio del módulo). Tampoco deben depender de ejecuciones
anteriores, salvo excepciones en que la propia claridad del programa obligue a lo contrario (por ejemplo, los
generadores de números aleatorios pueden depender de la llamada anterior).
Para el paso de parámetros entre módulos existen varios métodos que se exponen a continuación. Los parámetros pueden
pasarse además de dos maneras: directamente por valor, o bien indirectamente por referencia o dirección. En el primer
caso se envía el valor del parámetro y en el segundo la dirección inicial de memoria a partir de la que está almacenado.
El tipo de los parámetros habrá de estar debidamente documentado al principio de los módulos.
- Paso de parámetros en los registros: Los módulos utilizan ciertos registros muy concretos para comunicarse. Todos
los demás registros han de permanecer inalterados, por lo cual, si son empleados internamente, han de ser preservados
al principio del módulo y restaurados al final. Este es el método empleado por el DOS y la BIOS en la mayoría de las
ocasiones para comunicarse con quien los llama. Los registros serán preservados preferiblemente en la pila (con PUSH)
y recuperados de la misma (con POP en orden inverso); de esta manera, los módulos son reentrantes y pueden ser llamados
de manera múltiple soportando, entre otras características, la recursividad (sin embargo, se requerirá también que las
variables locales se generen sobre la pila).
- Paso de parámetros a través de un área común: se utiliza una zona de memoria para la comunicación. Este tipo de
módulos no son reentrantes y hasta que no acaben de procesar una llamada no se les debe llamar de nuevo en medio de
la faena.
- Paso de parámetros por la pila. En este método, los parámetros son apilados antes de llamar al módulo que los va
a recoger. Este debe conocer el número y tamaño de los mismos, para equilibrar el puntero de pila al final antes de retornar (método de los compiladores de lenguaje Pascal) o en caso contrario el programa que llama deberá encargarse de esta operación (lenguaje C). La ventaja del paso de parámetros por la pila es el prácticamente ilimitado número de parámetros admitido, de cómodo acceso, y que los módulos siguen siendo reentrantes. Un ejemplo puede ser el siguiente:
datoL DW ?
datoH DW ?
...
PUSH datoL ; apilar parámetros
PUSH datoH
CALL moduloA ; llamada
ADD SP,4 ; equilibrar pila
...
moduloA PROC NEAR
PUSH BP
MOV BP,SP
MOV DX,[BP+4] ; parte alta del dato
MOV AX,[BP+6] ; parte baja del dato
...
POP BP
RET
moduloA ENDP
En el ejemplo, tenemos la variable dato de 32 bits dividida en dos partes de 16. Dicha variable es colocada en la
pila empezando por la parte menos significativa. A continuación se llama a MODULOA, el cual comienza por preservar
BP (lo usará posteriormente) para respetar la norma de caja negra. Se carga BP con SP debido a que el 8086 no permite
el direccionamiento indexado sobre SP. Como la instrucción CALL se dirige a una dirección cercana (NEAR), en la pila
se almacena sólo el registro IP. Por tanto, en [BP+0] está el BP del programa que llama, en [BP+2] el registro IP del
programa que llama y en [BP+4] y [BP+6] la variable enviada, que es el caso más complejo (variables de 32 bits).
Dicha variable es cargada en DX:AX antes de proceder a usarla (también deberían apilarse AX y DX para conservar la
estructura de caja negra). Al final, se retorna con RET y el programa principal equilibra la pila aumentando SP en
4 unidades para compensar el apilamiento previo de dos palabras antes de llamar. Si MODULOA fuera un procedimiento
lejano (FAR) la variable estaría en [BP+6] y [BP+8], debido a que al llamar al módulo se habría guardado también en
la pila el CS del programa que llama. El lenguaje Pascal hubiera retornado con RET 4, haciendo innecesario que el
programa que llama equilibre la pila. Sin embargo, el método del lenguaje C expuesto es más eficiente porque no
requiere que el módulo llamado conozca el número de parámetros que se le envían: éste puede ser variable (de hecho,
el C apila los parámetros antes de llamar en orden inverso, empezando por el último: de esta manera se accede
correctamente a los primeros N parámetros que se necesiten).