Monografias.com > Computación > Programación
Descargar Imprimir Comentar Ver trabajos relacionados

Assembly language for PC (intel)




Enviado por Diego Bellini



    1. Conceptos
      básicos
    2. Registros de la
      CPU
    3. Modos de
      Direccionamiento
    4. Los Assemblers más
      comunes
    5. Instrucciones en
      Assembly
    6. Multiplicación y
      división, MUL y DIV:
    7. Ejercicios
      (1)
    8. Instrucción de salto
      incondicional, JMP
    9. FLAGS
    10. Intrucciones de
      comparación, CMP y TEST
    11. La Pila
    12. Llamados a subrutinas,
      CALL
    13. Respuestas de los
      ejercicios

    Este material es un trabajo práctico que se
    realizó en base a lo dictado en la materia
    "Arquitectura
    de las Computadoras
    II", que pertence a la carrera Analista Universitario en Sistemas, del
    Instituo Politécnico Superior Gral. San Martín
    (Universidad
    Nacional de Rosario, Argentina).

    Conceptos
    básicos

    Antes de comenzar, conviene aclarar que el lenguaje se
    denomina Assembly y no Assembler como normalmente se lo conoce.
    Cuando se escribe un programa en
    Assembly, hay otro programa "ensamblador"
    que se encarga de traducir lo diseñado por el programador
    al código
    de máquina. Esta clase de programa se llama traductor. Los
    compiladores e
    intérpretes son traductores que pasan a código
    máquina los programas
    escritos en los distintos lenguajes. Assembler es el programa
    traductor de Assembly a código de máquina, y que es
    propio de cada modelo de
    procesador. Cada
    microprocesador
    tiene su propio Assembler con su particular set de instrucciones
    y sus varios modos de direccionamiento que dependen de la
    arquitectura propia del sistema, y cada
    nuevo modelo de procesador que saca al mercado un
    fabricante, puede que agregue algunas instrucciones a su set de
    instrucciones para mejorar el rendimiento del mismo y hasta
    agregar algún nuevo registro (o
    extender la capacidad de bits de los existentes), pero siempre
    conservan las instrucciones y registros de los
    modelos
    anteriores por razones de compatibilidad.

    Dado que las mayoría de las PC usan procesadores
    Intel o sus clones, desarrollaremos ejercicios para esta
    arquitectura de procesadores.

    Registros de la
    CPU:

    Los primeros procesadores como el 8088 y el 8086 nos
    proveían de los registros de 16 bits AX, BX, CX, DX, SI,
    DI, BP, SP, CS, DS, SS, ES, IP, y FLAGS.
    El procesador soportaba hasta 1Mb de memoria y solo
    podía operar en modo Real. En este modo el programa
    podía acceder a cualquier dirección de memoria, incluso a direcciones
    utilizadas por otros programas. Esto hacía muy
    difícil la depuración del programa (debugging), y
    por lo tanto tampoco era muy seguro. Los
    datos de los
    programas se alojaban en memoria todo el tiempo y eran
    divididos en segmentos de hasta 64Kb.

    Sus cuatro registros principales, AX, BX, CX, y DX
    están divididos en dos registros de 8 bits cada uno. Por
    ejemplo, el registro AX posee una parte que contiene los primeros
    8 bits denominada AH (high) y una parte que contiene los
    últimos 8 bits denominada AL (low), y así
    sucesivamente con cada uno de los registros
    mencionados.

    16 bits

    8 bits

    8 bits

    AX

    AH

    AL

    BX

    BH

    BL

    CX

    CH

    CL

    DX

    DH

    DL

    Este tipo de registros se usa especialmente en operaciones
    aritméticas, ya que nos permite manejarnos con comodidad
    cuando trabajamos con datos que no superan un byte, pero se debe
    tener cuidado ya que AH y AL no son independientes de AX, o sea
    que si hacemos algún movimiento de
    datos referenciando a AX, también estamos cambiando
    los valores de
    AH y AL.

    Los registros SI y DI son utilizados generalmente como
    punteros.

    Los registros BP y SP se conocen como los punteros de
    pila. Se utilizan para moverse dentro de la pila.

    CS, DS, SS, y ES son los segments registers. Son los
    encargados de direccionar las distintas partes de cada
    programa:

    • CS para el code segment, donde se guardan los datos
      del código de máquina de las instrucciones que
      constituyen el programa.
    • DS para el data segment, que guarda los datos que el
      programa debe operar y los resultados de la ejecución
      del mismo.
    • SS para el stack segment, Almacena datos y
      direcciones necesarias durante la ejecución de cada
      parte del programa y que es localizada dentro del segmento
      mediante el registro SP (stack pointer).
    • ES para el extra segment, utilizado para guardar
      datos tipo strings, también como prolongación del
      DS (data segment), y como registro temporal. Cada dato es
      apuntado dentro del segmento por el registro puntero
      DI.

    Estos registros cumplían una importante función en
    el direccionamiento a memoria ya que el 8086 podía manejar
    solo hasta 1Mb de memoria (2bytes). El rango válido de direcciones de
    memoria era de 0x0000 a 0xFFFFF. Este rango de direcciones
    requería de un número de 20 bits, pero el 8086
    tenía registros de 16 bits. Intel resolvió el
    problema usando dos segment registers de 16 bits para determinar
    la dirección. A los primeros 16 bits se los denominaba
    selector y a los segundos offset. La dirección física real
    referenciada por el par de 32 bits formado por el
    selector:offset estaba dada por la
    fórmula:

    16* selector + offset

    Ejemplo:

    Queremos calcular la dirección física
    referenciada por el par 047C:0048.

    Primero debemos multiplicar a 047C * 16. Cuando
    multiplicamos por 16 en hexa, agregamos un cero a la derecha y
    listo (=047C0),

    luego hacemos la suma,

    047C0

    +0048

    04808

    y obtenemos que la dirección en memoria es la
    0x04808 (que es un nº de 20 bits.)

    El registro IP (instruction pointer) es utilizado para
    mantener una pista de la dirección de la próxima
    instrucción a ejecutarse por el procesador. Normalmente
    cuando se ejecuta una instrucción, IP se adelanta a
    apuntar a la próxima instrucción en
    memoria.

    El FLAGS es un registro de 16 bits que se divide en 16
    partes de 1 bit. Cada uno de estos bits guarda información importante sobre el resultado
    de la instrucción anterior. Este resultado se guarda con
    un solo un bit que puede ser 1 ó 0. Por ejemplo, el bit Z
    es 1 si el resultado de la instrucción anterior fue 0, y
    es 0 si el resultado fue 1. El bit C será 1 si la
    última operación produjo acarreo, y 0 si no lo
    hizo.

    El primer procesador para PC’s de clase AT fue el
    80286. Este incorporaba nuevas instrucciones a su set de
    instrucciones y presentaba su nueva forma de procesar en modo
    Protegido de 16 bits. En este modo el procesador podía
    acceder hasta a 16Mb de memoria y proteger a los programas entre
    sí en los accesos a la memoria. La
    configuración de sus registros era la misma que la de su
    antecesor, y los programas seguían estando divididos en
    segmentos de 64Kb. Pero estos segmentos ya no estaban en la
    memoria todo el tiempo, solo se mantenía a los que
    necesitaba el programa en el momento de ejecución. El
    resto de los datos y código era alojado en forma temporal
    en el disco hasta ser necesitados por el programa. Esta
    técnica se la conoce con el nombre de memoria
    virtual, y es la que se utiliza en la mayoría de los
    sistemas actuales. A cada segmento se le asigna un index dentro
    de una tabla descriptora (descriptor table), este index contiene
    toda la información que el sistema necesita conocer sobre
    el segmento, incluyendo si está corriendo actualmente en
    memoria, si está en memoria, si está en el disco,
    los permisos de acceso (lectura/escritura),
    etc. Este index identificador del segmento es un valor que se
    guarda en los segment registers.

    Con la aparición del 80386 se produce un gran
    salto en el diseño.
    Primero y principal se extiende la mayoría de sus
    registros a 32 bits (renombrándolos como EAX, EBX, ECX,
    EDX, ESI, EDI, EBP, ESP, EIP) y se agregan dos registros nuevos
    de 16 bits (FS, y GS). Para guardar compatibilidad con los
    diseños anteriores se conviene de que al hacer referencia
    a AX, se hace referencia a los últimos 16 bits de EAX (lo
    mismo que era AL de AX); pero no se puede tener acceso directo a
    los primeros 16 bits de EAX.

    Segundo, el procesador presenta su nuevo modo de trabajo
    denominado modo protegido de 32 bits. Este modo presenta dos
    grandes diferencias con el modo protegido de 16 bits:

    1. Se puede acceder hasta a 4Gb de memoria. Los
      programas se seguían dividiendo en segmentos, pero ahora
      cada segmento podía tener hasta 4Gb de
      tamaño.
    2. Los segmentos se pueden subdividir en pequeñas
      unidades de 4Kb de tamaño llamadas páginas. La
      memoria virtual trabaja ahora con páginas en lugar de
      segmentos. De esta manera, solo una parte del segmento puede
      estar en cualquier momento. En el modo protegido de 16 bits,
      solo se puede mover el segmento entero a la memoria, esto no
      hubiera sido muy práctico si consideramos los largos
      segmentos de 32 bits del 80386.

    Modos de
    Direccionamiento:

    Existen cuatro modos principales para manejar los datos
    entre los registros, las direcciones de memoria, y las
    constantes. NUNCA podemos pasar un dato constante a una
    dirección de memoria directamente, siempre deberemos
    llevar el dato a un registro, y luego de este a la
    memoria.

    Para ello debemos primero conocer que toda
    instrucción en Assembly, esta construida de la siguiente
    forma <instrucción>
    <destino>,<fuente>.

    Estos modos se denominan:

    • Modo directo: Se denomina así cuando en una
      instrucción se da directamente la dirección de
      memoria del dato a procesar. Ej.: MOV EAX,[5000] ,al
      estar el nro. 5000 entre [], le estamos indicando al procesador
      que mueva el dato que está en la dirección de
      memoria 0x5000 al registro EAX.
    • Modo registro: Cuando el dato que necesitamos
      procesar ya se encuentra en un registro.

    Ej.: MOV EAX,EBX ,aquí estamos indicando
    que lleve una copia del dato que está en EBX a
    EAX.

    • Modo inmediato: Se denomina así cuando
      ordenamos pasar una constante a un registro.

    Ej.: MOV EAX,3000 ,estamos pasando el nro. 3000
    al registro EAX.

    • Modo indirecto por registro: Cuando queremos pasar a
      un registro, el contenido del contenido de otro, es decir. EJ.:
      MOV EAX,[EBX], el contenido de la dirección de
      memoria almacenada actualmente en EBX, se cargará en
      EAX.

    Los Assemblers
    más comunes:

    • Netwide Assembler (NASM).
    • Microsoft Assembler (MASN).
    • Borland Assembler (TASM).

    Como existen algunas diferencias de sintaxis entre el
    MASM, TASM, y el NASM, hemos elegido a este último para el
    desarrollo de
    algunos de nuestros ejercicios de 32 bits. El mismo se puede
    obtener en forma gratuita de http://sourceforge.net/projects/nasm.

    Para los ejercicios basados en 16 bits, usaremos el
    DEBUG de DOS. Simplemente salimos desde cualquier versión
    de Windows al
    MS-DOS Prompt
    (o símbolo de sistema en la versiones 2000 y XP), y
    tipeamos DEBUG. Una vez que entramos al programa, elegimos una
    dirección de memoria inicial para cargar las instucciones
    (Ej.: A100), al finalizar con este proceso
    ingresamos ENTER y luego tipeamos la "t" para ver el estado de
    los registros en cada una de las instrucciones. Para salir
    tipeamos "q".

    Instrucciones
    en Assembly:

    Por fin vamos a empezar con instrucciones en Assembly. Y
    comenzaremos con la más sencilla, pero curiosamente la
    más utilizada en este lenguaje:

    La instrucción
    MOV:

    La función de la instrucción MOV, es como
    su nombre da a entender, "mover" un valor. Pongamos un
    ejemplo:

    MOV AX,BX

    Esta instrucción copia  el contenido de BX
    en AX, conservando el valor de BX.

    Veamos algunos ejemplos más:

    MOV AX,2000

    MOV [a100],AX

    En este caso introducimos el valor 2000 en AX, y luego
    lo llevamos a la dirección de memoria A100.

    En resumen, con la instrucción MOV podemos hacer
    las siguientes operaciones:

    Tipo de
    operación

    Ejemplo

    MOV registro,registro

    MOV AX,BX

    MOV memoria,registro

    MOV [2000],AX

    MOV registro,memoria

    MOV BX,[1500]

    MOV registro,constante

    MOV AX,1000

    Las instrucciones INC y DEC:

    Son las más básicas a la hora de hacer
    operaciones con registros:

    INC, incrementa el valor de un registro (o de cualquier
    posición de memoria ) en una unidad.

    DEC, lo decrementa en el mismo valor.
    Ejemplos:

    INC AX

    DEC AX

    Incrementa en una unidad el valor de AX y lo decrementa,
    por lo que no ha cambiado su valor.

    Estas dos instrucciones nos van a servir mucho en el uso
    de contadores en los bucles. Son equivalentes a ++ y — del
    lenguaje
    C.

    Las instrucciones ADD y SUB:

    Se trata de dos operadores que contienen todos los
    lenguajes de
    programación: la suma y la resta. Tienen dos
    operandos, uno de destino y otro fuente.

    Para la suma, se suman los dos operandos y se almacena
    el resultado en el primero (destino).

    Para la resta, se le resta al primero el segundo, y se
    almacena el resultado en el primero (destino).

    Ejemplos:

    ADD AX,BX ;Suma a AX con BX, y lo guarda en AX. (AX+BX)
    -> AX

    ADD AX,[SI] ;Se suman AX y el contenido de lo que esta
    apuntado por SI

    ;y se almacena en AX. (AX+[SI]) -> AX

    ADD AX,3 ;Suma 3 a AX y lo
    almacena en AX. (AX+3) -> AX

    SUB AX,AX ;Resta AX de AX. Se utiliza para poner a AX
    en 0.

    ;(AX-AX=0) -> AX

    SUB CX,DX ;Se resta el valor de CX con DX y se almacena
    en CX.

    ;(CX-DX) -> CX

    SUB CX,20 ;Se resta de CX el valor 20, y queda en CX el
    resultado.

    ;(CX-20) -> CX

    (Introducimos el uso de la
    ";" para hacer comentarios. Es
    lo mismo que usar en lenguaje C la //
    ó /*…….*/)

    Estas operaciones pueden modificar los FLAGS de estado si se
    producen acarreos en las sumas o números negativos en las
    restas.

    Estos casos los trataremos más
    adelante.

    Multiplicación y división, MUL y
    DIV:

    Estas operaciones multiplican o dividen al acumulador
    (AX) por el operando indicado. Si el operando es de 8 bits (1
    byte ), el acumulador es AL. Si el operando es de 16 bits, el
    acumulador es AX. En el caso de procesadores de 32 bits, si el
    operando tiene 8 ó 16 bits el acumulador será AX.
    El resultado se almacena en AX, ó en el par DX, AX
    respectivamente, si el operando es de 8 bits ó 16
    bits.

    En el caso de la división, si hubiera resto se
    guardará en DX.

    Ejemplos:

    Multiplicación de dos números de 8
    bits:

    MOV CL,20 ;Cargo en CL un número de 8 bits
    (0x20)

    MOV AL,30 ;Cargo en AL un número de 8 bits
    (0x30)

    MUL CL ;Multiplico CL por AL, y guardo el resultado en
    AX

    ;(AL*CL) -> AX

    Multiplicación de dos números de 16
    bits:

    MOV BX,1000 ;Cargo en CX un número de 16 bits
    (0x2000)

    MOX AX,1200 ;Cargo en AX un número de 16 bits
    (0x1200)

    MUL BX ;Multiplico BX por AX, y guardo el resultado en
    DX (mitad

    ;superior y en AX (mitad inferior). (AX*BX) -> (DX y
    AX)

    División de dos números de 8
    bits:

    MOV AX,30 ;Cargo en AX un número de 8 bits
    (0x30)

    MOV BX,12 ;Cargo en BX un número de 8 bits
    (0x10)

    DIV BX ;Divido AX/BX, y guardo el resultado en AX. El
    resto de la

    ;división se guardará en DX

    División de un número de 32 bits por uno
    de 16 bits:

    MOV AX,5000 ;Cargo en AX los 16 bits inferiores de un
    número de 32

    ;bits (0x65005000)

    MOV DX,6500 ;Cargo en DX los 16 bits superiores del
    número

    ;anterior (0x65005000)

    MOV CX,2500 ;Cargo en CX un número de 16 bits que
    será el divisor

    ;(0x2500)

    DIV CX ;Divido (DX,AX)/CX y guardo el resultado en AX.
    El

    ;resto de la división se guardará en
    DX

    Pero el procesador puede diferenciar entre operaciones
    con números con signo y sin signo. Cuando utilicemos
    números que involucran las operaciones
    multiplicación y división de números con
    signo, utilizaremos las instrucciones IMUL y IDIV
    respectivamente.

    Ejercicios
    (1)
    :

    Son algunos ejercicios de operaciones aritméticas
    y modos de direccionamiento:

    1) Indique que realizan las siguientes instrucciones,
    aclarando en cada caso los modos de direccionamiento
    utilizados:

    MOV AX,10

    MOV BX,[100]

    ADD CX,[AX]

    ADD BX,2

    DEC CX

    2) Realice un programa que haga la siguiente
    operación A=B+B-C. El dato B se encuentra en la
    dirección 5000, el C en 5006, y el resultado A hay que
    alojarlo en 5010.

    3) Realice un programa que haga la siguiente
    operación A=((B+C)*B)/C. Utilice los mismos datos del
    ejercicio anterior, haciendo que el resto de la división
    se aloje en la dirección 2000.

    Operaciones lógicas NEG, NOT, AND, OR,
    XOR:

    NEG, pone el registro o el contenido de una
    dirección de memoria en negativo según la
    aritmética de complemento a dos (NEG AX, NEG
    [2000]).

    NOT, ordena invertir cada bit del operando. Supongamos
    que AX = 0x1000, al hacer NOT AX, el contenido de AX
    cambiará a 0xEFFF.

    AND, realiza la operación lógica
    entre dos valores y
    guarda el resultado en el valor destino.

    OR, realiza la operación lógica entre dos
    valores y guarda el resultado en el valor destino.

    XOR, realiza la operación lógica entre dos
    valores y guarda el resultado en el valor destino.

    Instrucción de
    salto incondicional, JMP:

    Se puede cambiar el control a
    cualquier punto del programa sin condiciones.

    Se utiliza de la siguiente forma:

    JMP 03424h

    Donde 03424h, es la dirección a la cual queremos
    saltar.

    ¿Pero como hacemos para calcular la
    dirección de donde va a estar la instrucción a la
    cual queremos avanzar?

    Para ello utilizaremos etiquetas y saltaremos
    directamente al nombre de la etiqueta.

    Ejemplo: (entre paréntesis se muestra el orden
    de ejecución de estas instrucciones)

    (1) MOV AX,1000 ;Cargo en AX el número
    0x1000

    (2) MOV CL,22 ;Cargo en CL el número
    0x22

    (3) JMP AVANZA ;Llamo a la
    instrucción etiquetada como "AVANZA"

    (6) VUELVE: ADD BX,AX ;Sumo a BX el valor de AX
    (BX=0x2000)

    (7) JMP FIN ;Llamo a la instrucción etiquetada
    como "FIN"

    (4) AVANZA: MOV BX,AX ;Cargo en BX el valor de AX
    (BX=0x1000)

    (5) JMP VUELVE ;Llamada a la etiqueta
    "VUELVE"

    (8) FIN: SUB CX,BX ;Sumo a CX el valor de BX
    (CX=0x2022)

    Las etiquetas tienen que estar continuadas por dos
    puntos ':', y pueden ser llamadas desde cualquier lugar del
    programa. También podremos hacer un MOV AX,[AVANZA], como
    hacíamos antes con un MOV AX,[BX], pero asignando a AX el
    valor que haya en la dirección de memoria en la que
    está "AVANZA".

    FLAGS:

    La explicación de los "flags" está
    relacionada con el uso de los saltos condicionales. Utilizaremos
    algunos de estos flags para instrucciones donde haya que tomar
    decisiones (del tipo IF-THEN-ELSE, WHILE, ó FOR en otros
    lenguajes).

    Como se explicó en la introducción de este apunte, los flags
    (banderas) los agrupa un solo registro de 16 bits (denominado
    registro de estados S). Aunque este no esté utilizado por
    completo, ya que cada flag ocupa un solo bit, cada uno de estos
    pueden valer 1 ó 0 y dependiendo de su valor indican
    varias cosas. El registro de flags es el siguiente:

    Número de
    bit

    Nombre

    Utilización

    0

    C

    Carry. Se pone en 1 si se produjo acarreo en la
    última operación realizada.

    1

    No utilizado.

    2

    P

    Paridad. Se utiliza especialmente en la
    transmisión de datos para la comprobación
    de errores, ya que comprueba si el resultado de la
    última operación aritmética ó
    lógica realizada tiene un número par o
    impar de bits puestos a uno. Se pondrá en 1 cuando
    haya un número par de bits, y a 0 cuando sea
    impar.

    3

    No utilizado.

    4

    A

    Auxiliar Carry. Se utiliza como verificador
    auxiliar de acarreos.

    5

    No utilizado.

    6

    Z

    Zero. Es el que más interviene en las
    operaciones con bucles. Simplemente se activa a 1 cuando
    el resultado de una operación aritmética
    ó lógica es cero o devuelve verdadero. Por
    ejemplo, si tenemos dos valores iguales en CX y AX y
    hacemos SUB CX,AX el flag Z se pondrá en 1. Lo
    mismo pasa con una operación lógica, si
    esta es verdadera, Z se pondra en 1.

    7

    S

    Sign (o signo). Indica cuando tras una
    operación aritmética ó lógica
    el resultado es un número en complemento a dos,
    por lo tanto, cuando vale 1 es que el número es
    negativo y si vale 0 es positivo.

    8

    T

    Trap. Trampa, si está activado ejecuta
    una interrupción INT 1h cada vez que quiera
    ejecutarse otra instrucción.

    9

    I

    Interruption. Es el flag de interrupción,
    cuando está activado evita la posibilidad de
    interrupciones en secciones críticas de
    código,

    10

    D

    Flag de rumbo ó dirección y
    sentido. Determina si se han de autoincrementar ó
    autodecrementar los punteros SI y DI.

    11

    O

    Overflow (o desbordamiento). Es bastante
    parecido al de acarreo pero actúa con
    números en complemento a dos, y se activa cuando
    se pasa del mayor número positivo (127 en un solo
    byte), al menor negativo (-128 en un solo byte). Este
    flag, al contrario que el de acarreo, sí es
    afectado por las instrucciones de incremento y
    decremento.

    12

    No utilizado.

    13

    No utilizado.

    14

    No utilizado.

    15

    No utilizado.

    Intrucciones de
    comparación, CMP y TEST:

    CMP compara dos registros, o un registro y una
    dirección de memoria. Tiene el mismo formato que el SUB
    (por ejemplo CMP AX,BX), tan solo que ninguno de los registros es
    alterado. Si por ejemplo son iguales, el flag de cero Z se
    pondrá en 1. Funciona como un SUB del que no se almacena
    el resultado.

    TEST realiza una comprobación, trabaja igual que
    AND, pero no guarda el resultado en ningún lado, aunque
    sí se modifican los flags.

    Saltos condicionales:

    Los saltos condicionales dependen exclusivamente de los
    Flags, son condicionados al estado 1 ó 0 del Flag que
    verifican, y son los siguientes:

    (Aclaración: todos los saltos se preceden de la
    comparación que desea verificar una condición,
    dichas comparaciones se expresan comparando al primer miembro con
    el segundo)

    JO:

    Jump if Overflow. Salta si el flag de
    desbordamiento está en 1.

    JNO:

    Jump if Not Overflow. Salta si el flag de
    desbordamiento esta en 0.

    JC:

    Jump if Carry. Salta si el flag de acarreo es
    1.

    JNAE:

    Jump if Not Above or Equal. Salta si no es mayor
    o igual.

    JB:

    Jump if Below. Salta si es menor.

    JNC:

    Jump if Not Carry. Salta si el flag de acarreo
    es 0.

    JAE:

    Jump if Above or Equal. Salta si es mayor o
    igual.

    JNB:

    Jump if Not Below. Salta si no es
    menor.

    JZ:

    Jump if Zero. Salta si el flag de cero Z es
    1.

    JE:

    Jump if Equal. Salta si dos instrucciones
    comparadas son iguales (CMP 0,0)

    JNZ:

    Jump if Not Zero. Salta si el flag de cero Z es
    0.

    JNE:

    Jump if Not Equal. Salta si dos instrucciones
    comparadas son distintas (CMP 3,4)

    JBE:

    Jump if Below or Equal. Salta si es menor o
    igual.

    JNA:

    Jump if Not Above. Salta si no es
    mayor.

    JA:

    Jump if Above. Salta si es mayor.

    JNBE:

    Jump if Not Below or Equal. Salta si no es menor
    o igual.

    JS:

    Jump if Sign. Salta si el flag de signo S es
    1.

    JNS:

    Jump if Not Sign. Salta si el flag de signo S es
    0.

    JP:

    Jump if Parity. Salta si el flag de paridad P es
    1.

    JPE:

    Jump if Parity Even. Idem anterior.

    JNP:

    Jump if Not Parity. Salta si el flag de paridad
    P es 0.

    JPO:

    Jump if Parity Odd. Idem anterior.

    JL:

    Jump if Less. Salta si es menor. (para
    números con signo)

    JNGE:

    Jump if Not Greater or Equal. Salta si no es
    mayor o igual. (para números con signo)

    JGE:

    Jump if Greater or Equal. Salta si es mayor o
    igual. (para números con signo)

    JNL:

    Jump if Not Less. Salta si no es menor. (para
    números con signo)

    JLE:

    Jump if Lower or Equal. Salta si es menor o
    igual. (para números con signo)

    JNG:

    Jump if Not Greater. Salta si no es mayor. (para
    números con signo)

    JG:

    Jump if Greater. Salta si es mayor. (para
    números con signo)

    JNLE:

    Jump if Not Lower or Equal. Salta si no es menor
    o igual. (para números con signo)

    Veamos algunos ejemplos de los más
    utilizados:

    MOV AX,1111 ;Cargo 0x1111 en AX

    MOV BX,1112 ;Cargo 0x1112 en BX

    CMP AX,BX ;Comparo AX con BX (en este caso AX es menor
    que BX)

    JB AVANZA ;Saltar a "AVANZA" (si AX<BX)

    AVANZA: DEC BX ;Ahora BX vale 0x1111

    CMP AX,BX ;Comparo AX con BX (ahora valen lo
    mismo)

    JNE FIN ;Saltar a "FIN" (si AX!=BX, como son iguales
    no salta)

    JE CONT ;Saltar a "CONT" (si AX=BX, como son iguales
    salta)

    CONT: DEC BX ;Ahora BX vale 0x1110

    CMP AX,BX ;Comparo AX con BX (ahora AX es mayor que
    BX)

    JE FIN ;Saltar a "FIN" (si
    AX=BX, como no son iguales no salta)

    JB FIN ;Saltar a "FIN" (si
    AX<BX, como no es menor, no salta)

    JG FIN ;Saltar a "FIN" (si
    AX>BX, Es mayor, ahora si salta)

    FIN: SUB AX,AX ;Pone en cero AX

    XOR BX,BX ;Hace XOR de BX con BX (otra manera de poner
    en cero)

    La Pila:

    La pila es una especie de "almacén de
    variables" que
    se encuentra en una dirección determinada de memoria
    (dirección que viene indicada por SS:SP).

    Entonces nos encontramos con dos órdenes
    básicas respecto al manejo de la pila, que son PUSH y POP.
    La orden PUSH empuja una variable a la pila, y la orden POP la
    saca.

    Sin embargo, no podemos sacar el que queramos, debemos
    cuidar el orden natural de la estructura de
    una pila. Esta se denomina LIFO (siglas en inglés
    que indican "Last In First Out"). Esto significa que al hacer un
    POP, se saca el último valor introducido en la
    pila.

    Veamos un par de ejemplos:

    PUSH DX ;Mete en la pila el contenido de DX.

    PUSH CX ;Mete en la pila el contenido de CX.

    POP AX ;Saca de la pila su último valor (CX), y
    lo coloca en AX

    POP BP ;Saca de la pila su último valor (DX), y
    se lo asigna a BP.

    MOV DX,300 ;Cargo en DX el número
    0x0300.

    PUSH DX ;Empuja DX a la pila (0x0300).

    MOV CX,200 ;Cargo en CX el número
    0x0200.

    PUSH CX ;Meto en la pila el contenido de CX
    (0x0200).

    POP AX ;Saco de la pila el número 0x0200 y lo
    cargo en AX.

    POP BX ;Saco de la pila el número 0x0300 y lo
    cargo en BX.

    ADD AX,BX ;Sumo BX en AX. AX vale 0x0500.

    La pila se puede operar con los registros AX, BX, CX,
    DX, SI, DI, BP, SP, CS, DS y ES, sin embargo no se puede hacer un
    POP CS, solamente empujarlo a la pila.

    Como última recomendación, hay que tener
    bastante cuidado con los PUSH y POP, sacar tantos valores de la
    pila como se metan, y estar pendiente de que lo que se saca es lo
    que se tiene que sacar. La pila bien aprovechada es fundamental
    para hacer programas bien optimizados.

    Y finalmente, hay otras dos órdenes interesantes
    respecto a la pila, PUSHF y POPF, que empujan el registro de
    estado de 16 bits (flags) a la pila, y lo saca
    respectivamente.

    Llamados a
    subrutinas, CALL:

    Se trata de una orden que se utiliza para llamar a
    subrutinas, y está relacionada con el uso de la
    pila,

    La sintáxis del CALL es casi la de un JMP,
    pudiéndose también utilizar etiquetas, direcciones
    inmediatas o registros.

    Su forma de trabajo es sencilla. Empuja a la pila los
    valores de CS e IP (o sea, los del punto en el que estén
    en ese momento del programa) aunque IP aumentado en el
    tamaño del call para apuntar a la siguiente
    instrucción, y hace un salto a la dirección
    indicada. Cuando encuentre una instrucción RET,
    sacará CS e IP de la pila, y así retornará
    al lugar de origen. Veamos un ejemplo:

    SUB AX,AX ;Ax vale ahora 0

    CALL CHAU ;Mete CS e IP a la pila y salta a
    "CHAU"

    INT 20 ;Finaliza

    CHAU: MOV AX,30 ;Carga un número en AX

    RET ;Saca a CS e IP de la pila y vuelve a la
    instrucción

    ;siguiente al punto de llamada, o sea, a "INT
    20"

    Ejercicios (2):

    Luego de la introducción de todos estos
    conceptos, vamos a hacer unos ejercicios.

    1) Realice un programa que calcule al producto de
    dos números, como una suma. M*N=(M+M+M+……M) (N veces).
    (M está en 3000, N en 2000, y el resultado guardarlo en
    AX)

    2) Dada una lista de números de un byte que
    empieza en la dirección 2000, y su longitud total esta
    guardada en la dirección 1500, encuentre el número
    mayor de toda la lista y guárdelo en la dirección
    3000.

    3) Se tiene una lista de números que comienzan en
    la dirección 2000, y su longitud está en 1500.
    Contar cuantos números negativos y positivos hay y
    separarlos en dos listas que empiecen en 3000 y 4000
    respectivamente. El total de negativos guardarlo en 1600, y el de
    positivos en 1700.

    4) Se tiene una lista de números enteros de un
    byte cada uno que empieza en la dirección 2000, y su
    longitud está en 1500. Pasar los números a otra
    lista donde cada número ocupe 16 bits, de modo que a los
    números con bit de signo 0 se le agreguen 8 ceros a la
    izquierda, y a los números con bit de signo 1 se le
    agreguen 8 unos a la izquierda.

    5) Realice el cálculo
    del determinante de una matriz de
    2×2.

    6) Realice el cálculo del determinante de un
    matriz NxN. Ayuda: Intente comparar los pasos que va realizando,
    con los hechos en otro lenguaje de más alto nivel (tipo C,
    ó C++)

    Respuestas de los ejercicios

    Ejercicios sección (1):

    1)

    1º) Cargo en AX el valor 0x10 en modo
    inmediato.

    2º) Cargo en BX el valor que está almacenado
    en la dirección de memoria 0x0100 en modo
    directo.

    3º) Sumo a CX el valor que está en la
    dirección de memoria 0x0010, ya que por el 1er. paso AX
    tiene el dato 0x10. Este modo se denomina indirecto por
    registro.

    4º) Sumo 2 a BX.

    5º) Resto uno a CX.

    2)

    MOV AX,[5000] ;Cargo en AX el dato B que está en
    5000 y 5001 de memoria

    ADD AX,[5000] ;Sumo en AX el dato B que está en
    5000 y 5001 de memoria

    SUB AX,[5006] ;Resto a AX el dato C que está en
    5006 y 5007 de memoria

    MOV [5010],AX ;Cargo en 5010 y 5011 de memoria, el
    contenido de AX

    3)

    MOV AX,[5000] ;Cargo en AX el dato B que está en
    5000 y 5001 de memoria

    ADD AX,[5006] ;Sumo a AX el dato C que está en
    5006 y 5007 de memoria

    MOV CX,[5000] ;Cargo en CX el dato B

    IMUL CX ;Hace la operación CX*AX y guarda el
    resultado en (DX,AX)

    MOV CX,[5006] ;Cargo en CX el dato C que está en
    5006 y 5007 de memoria

    IDIV CX ;Realizo la división
    (DX,AX)/CX

    MOV [5010],AX ;Llevo a la dirección 5010 el
    resultado que está en AX

    MOV [2000],DX ;Llevo a la dirección 2000 el resto
    de la división que

    ;está en DX.

    Ejercicios de la sección
    (2):

    1)

    MOV CX,[2000] ;Llevo a CX el valor de N

    MOV BX,[3000] ;Llevo a BX el valor de M

    SUB AX,AX ;Pongo en cero a AX

    RUTINA: ADD AX,BX ;Sumo M a AX

    DEC CX ;Resto uno a CX (cuando CX sea 0, Z=1)

    JNZ RUTINA ;Si luego de la instrucción anterior
    Z=0, saltar a

    ;"RUTINA"

    INT 20 ;Fin

    2)

    MOV CL,[1500] ;Cargo en CL la longitud de la
    lista

    MOV SI,2000 ;Apunto SI al comienzo de la lista de
    datos

    MOV AL,[SI] ;Llevo el primer elemento de la lista a
    AL

    INC SI ;Incremento el puntero para buscar el segundo
    dato

    DEC CL ;Decremento el contador de elementos de la
    lista

    VOLVER: CMP AL,[SI] ;Comparo al número que
    está en AL con el que está

    ;siendo apuntado por SI

    JA RUTINA ;Si AL>[SI] ir a "RUTINA"

    MOV AL,[SI] ;Carga en AL el nuevo número que
    apunta SI

     RUTINA: INC SI ;Incrementa SI para
    buscar el próximo elemento

    DEC CL ;Decrementa el contador de elementos de la
    lista

    JNZ VOLVER ;Mientras Z=0, volver a "VOLVER"

    MOV [3000],AL ;Guarda en 3000 el resultado (el
    número mayor)

    INT 20 ;Finaliza

    3)

    MOV CL,[1500] ;Cargo la longitud de la lista en el
    contador

    MOV SI,2000 ;El registro SI apunta al comienzo de la
    lista

    MOV DI,3000 ;DI apunta al comienzo de la lista que
    guardará

    ;los negativos

    MOV BX,4000 ;BX apunta al comienzo de la lista que
    guardará

    ;los positivos

    SUB DX,DX ;Llevo DX a cero

    SUB_1: MOV AX,[SI] ;Cargo en AX el primer dato apuntado
    por SI

    CMP AX,0 ;Comparo este dato con el 0

    JL SUB_2 ;Si AX<0, saltar a "SUB_2"

    JG SUB_3 ;Si AX>0, saltar a "SUB_3"

    SUB_4: ADD SI,2 ;Suma 2 bytes a SI para buscar el
    próximo elemento

    DEC CL ;Decrementa el contador

    JNZ SUB_1 ;Si Z=0, volver a "SUB_1"

    MOV [1600],DL ;Guarda el negativo en 1600

    MOV [1700],DH ;Guarda el positivo en 1700

    INT 20 ;Fin

    SUB_2: INC DL ;Incrementa a DL

    MOV [DI],AX ;Guarda el negativo en la lista apuntada por
    DI

    ADD DI,2 ;Suma 2 bytes a DI

    JMP SUB_4 ;Saltar a "SUB_4"

    SUB_3: INC DH ;Incrementa a DH

    MOV [BX],AX ;Guarda el positivo en la lista apuntada por
    BX

    ADD BX,2 ;Suma 2 bytes a BX

    JMP SUB_4 ;Saltar a "SUB_4"

    4)

    MOV CL,[1500] ;Cargo en CX la longitud de la
    lista

    MOV SI,2000 ;Apunto a SI al comienzo de los
    datos

    MOV DI,3000 ;Apunto DI al comienzo de la lista
    resultados

    CARGA: MOV AL,[SI] ;Carga en AL el primer elemento de la
    lista

    CMP AL,0 ;Compara AL contra 0

    JGE SUB_1 ;Si AL>=0, saltar a "SUB_1"

    JL SUB_2 ;Si AL<0, saltar a "SUB_2"

    REFRESH: INC SI ;Incremento SI para buscar el
    próximo elemento

    ADD DI,2 ;Sumo 2 a DI (los resultados ocupan 2
    bytes)

    DEC CL ;Decremento en uno el contador (CL=0 ->
    Z=1)

    JNZ CARGA ;Si Z=0, saltar a "CARGA"

    INT 20 ;Fin

     SUB_1: MOV AH,00 ;Carga 8 ceros en
    AH

    MOV [DI],AX ;Carga el número resultante en la
    lista que

    ;apunta DI (número de 16 bits)

    JMP REFRESH ;Saltar a "REFRESH"

    SUB_2: MOV AH,FF ;Carga 8 unos en AH

    MOV [DI],AX ;Carga el número resultante en la
    lista que

    ;apunta DI (número de 16 bits)

    JMP REFRESH ;Saltar a "REFRESH"

    5)

    ;NASM-IDE ASM Assistant Assembler Project
    File

    BITS 16 ;Set code generation to 16 bit mode

    ORG 0x0100 ;Set code start address to 0100h

    SEGMENT .data ;Initialised data segment

    matriz1 dw 1,2,3,4 ;matriz inicializada con datos
    1,2,3,4

    inicioFila dw 0 ;por donde comienzo a
    multiplicar

    SEGMENT .bss ;Uninitialised data segment

    matriz2 resw 4 ;reservo espacio para la matriz
    resultado

    resul resw 1

    contador resw 1

    SEGMENT .text ;Main code segment

    mov si,matriz1

    mov di,matriz2

    call CALCULAR

    mov word
    [inicioFila],4 ;siempre debo especificar el tipo cuando son
    cte

    call CALCULAR

    mov ax, $4C00 ;Prepare to exit

    int 20 ;Terminate program

    CALCULAR:

    xor cx,cx ;inicializo contador

    BUCLE:

    mov [contador],cx ;guardo contador ya que voy a usar
    cx

    mov bx,[inicioFila] ;por que fila examino la
    matriz1

    mov cx,[si+bx] ;valor en la posici¢n
    examinada

    mov bx,[contador] ;por que columna examino
    matriz1

    mov ax,[si+bx] ;valor

    imul cx ;multiplico

    mov [resul],ax ;guardo resultado parcial

    mov bx,[inicioFila] ;restauro bx

    mov cx,[si+bx+2] ;por que fila examino la
    matriz1

    mov bx,[contador] ;por que columna examino
    matriz1

    mov ax,[si+bx+4] ;valor

    imul cx ;multiplico

    add [resul],ax ;obtengo resultado final Aij

    mov bx,[inicioFila] ;fila

    add bx,[contador] ;adiciono a fila, la
    columna

    mov ax,[resul] ;coloco el resultado final en
    ax

    mov [di+bx],ax ;guardo resultado en matriz2

    mov cx,[contador] ;restauro valor contador

    add word cx,2 ;incremento en 1 word el contador (2
    bytes)

    cmp cx,6 ;si contador != 6

    jne BUCLE ;volver a BUCLE

    ret ;si no, regresar

    6) Antes de resolver este ejercicio, es bueno
    estudiar un poco el proceso de cálculo en un lenguaje de
    mayor nivel, ya que son muchos los llamados a subrutinas y va a
    resultar un poco engorroso entederlo directamente. Este ejemplo
    esta realizado en C++.

    int det(int *M,int N){

    if(N==2){

    return((M[0]*M[3])–(M[1]*M[2]));

    }

    else{

    int new_dimension=(N-1);

    int tdet =0;

    int *temp =new int[(N-1)*(N-1)];

    for(int ref_ele=0;ref_ele<N;ref_ele++){

    int counter=0;

    for(int i=0;i<N;i++){

    for(int j=0;j<N;j++){

    if((j!= ref_ele)&&(i!=0)){

    temp[counter]=M[(N*i + j)];

    counter++;

    }

    }

    }

    int t = det(temp, new_dimension);

    if((ref_ele % 2)!= 0)

    t =(-1)*t;

    tdet += M[ref_ele]*t;

    }

    return tdet;

    }

    }

    El algoritmo
    trabaja haciendo llamados recursivos de sub-cálculos si
    N>2 hasta que sea N=2 para hacer el cálculo en forma
    directa de filas por columnas. Para cada matriz M(i,j), se
    necesita determinar la sub-matriz que queda formada de no
    contener a la fila y columna del dato que queremos
    calcular

    SEGMENT .bss

    M resw 153 ;Reservo espacio para las matrices
    temporales

    ;(25+(5*16)+(4*9)+(4*3))

    N resw 1 ;Reservo espacio para el N de la
    matriz

    SEGMENT .text

    MOV [N],3 ;cargo el orden de la matriz en N (Ej.
    3)

    MOV SI,M ;inicio de la matriz

    MOV AX,[N] ;cargo el N de la matriz en ax (3)

    PUSH SI ;coloco en la pila a si

    PUSH AX ;coloco en la pila a ax (N)

    CALL det ;Llamo a det

    ADD SP,4 ;renuevo parametros de la pila

    INT 20 ;Fin

    det:

    PUSH BP ;guardo el estado del puntero base

    MOV BP,SP ;apunto bp al tope de la pila

    CMP [BP+4],2 ;comparo si N=2

    JNE else ;si no es igual, ir a else

    MOV SI,[BP+6] ;si es igual, comienza el calculo
    empezando por apuntar

    ;a SI al comienzo de la matriz de 2×2 (elemento
    a00)

    MOV AX,[SI] ;cargo en ax el primer elemento de la matriz
    (el a00)

    MOV BX,[SI+6] ;cargo en bx el ultimo elemento de la
    matriz (el a11)

    IMUL BX ;multiplico bx*ax (a00 * a11)

    MOV CX,AX ;guardo el resultado de la primera
    multiplicacion en cx

    MOV AX,[SI+2] ;cargo en ax el ultimo elemento de la fila
    0 (el a01)

    MOV BX,[SI+4] ;cargo en bx el primer elemento de la fila
    1 (el a10)

    IMUL BX ;multiplico bx*ax (a01 * a10)

    SUB CX,AX ;resto el resultado de ambas
    multiplicaciones

    MOV AX,CX ;devuelvo el resultado final en ax

    MOV SP,BP ;restauro el tope de la pila con el valor
    anterior de

    ;la funcion

    POP BP ;restauro el puntero base con el valor anterior
    de la

    ;funcion

    RET ;Retorno

    else:

    SUB SP,16 ;guardo espacio para 8 variables que voy a
    utilizar

    MOV AX,[BP+4] ;guardo en ax el valor N de la matriz
    original

    DEC AX ;decremento AX

    MOV [BP-2],AX ;bp-2 es una nueva variable (newdim) que
    contiene la

    ;nueva dimension de la matriz (N-1)

    MOV [BP-4],0 ;bp-4 es una nueva variable =0 (dettemp)
    que usare como

    ;determinante temporal

    MOV SI,[BP+6] ;apunto si al comienzo de la matriz
    (*M)

    MOV AX,[BP+4] ;guardo el orden de la matriz

    IMUL AX ;opero N * N (en este ejemplo = 9)

    MOV BX,2 ;cargo 2 en bx

    IMUL BX ;multiplico ax*2 (=18, bytes que utilizare
    para

    ;reservar en memoria)

    ADD SI,AX ;apunto si al comienzo de la nueva matriz
    temporal

    ;(+ 18 bytes)

    MOV [BP-6],SI ;bp-6 es una nueva variable (*temp) que
    contiene la

    ;dirección de la nueva matriz temporal (o
    adjunta)

    MOV [BP-8],0 ;bp-8 es una nueva variable (que llamare
    ref_ele)

    for1:

    MOV CX,[BP+4] ;cargo N en cx

    CMP [BP-8],CX ;comparo si ref_ele es < N

    JNL endfor1 ;Saltar si [BP-8] no es menor que
    CX

    MOV [BP-10],0 ;bp-10 es una nueva variable =0
    (contador)

    MOV [BP-12],0 ;bp-12 es una nueva variable =0
    (i)

    for2:

    CMP [BP-12],CX ;comparo a i con N

    JNL endfor2 ;saltar si [BP-12] no es menor que
    CX

    MOV [BP-14],0 ;bp-14 es una nueva variable =0
    (j)

    for3:

    CMP [BP-14],CX ;comparo a j con N

    JNL endfor3 ;saltar si [BP-14] no es menor que
    CX

    MOV AX,[BP-14] ;en ax cargue j

    if1:

    CMP AX,[BP-8] ;comparo si j es != ref_ele

    JE endif1 ;Saltar si AX=[BP-8]

    CMP [BP-12),0 ;comparo si i!=0

    JE endif1 ;saltar si [BP-12]=0

    MOV AX,[BP-12] ;cargo i en ax

    IMUL CX ; N * i

    ADD AX,[BP-14] ; N * i + j (formula para recorrer
    matrices)

    MOV BX,2 ;cargo 2 en BX

    IMUL BX ;multiplico a ax por 2 ya que recorro la matriz
    de a 2

    ;bytes

    MOV BX,AX ;guardo el desplazamiento de N*i+j en
    bx

    MOV SI,[BP+6] ;apunto si a M (primer
    elemento)

    MOV DX,[SI+BX] ;coloco en dx M[(N*i+j)]

    MOV BX,[BP-10] ;coloco en bx a contador

    MOV SI,[BP-6] ;apunto si a la matriz temporal

    MOV [SI+BX],DX ;temp[contador]=M[(N*i+j)]

    ADD [BP-10],2 ;actualizo a contador (+2
    bytes)

    endif1:

    ADD [BP-14],2 ;j++

    JMP for3

    endfor3:

    ADD [BP-12],2 ;i++

    JMP for2

    endfor2:

    MOV BX,[BP-6] ;inicio de matriz

    MOV AX,[BP-2] ;orden matriz

    PUSH BX ;coloco este parametro en pila

    PUSH AX ;coloco este parametro en pila

    CALL det ;Llamo a det

    ADD SP,4 ;renuevo la pila con los parametros
    anteriores

    MOV [BP-16],AX ;devuelvo el resultado de la funcion en
    ax

    if2:

    MOV AX,[BP-8] ;ax=ref_ele

    MOV BX,2 ;cargo 2 en bx

    IDIV BX ;relizo división

    CMP DX,0 ;(ref_ele%2)!=0

    JE endif2 ;saltar si DX=0

    MOV AX,[BP-16] ;ax=t

    MOV BX,-1 ;cargar –1 en BX

    IMUL BX ;t=(-1)*t

    endif2:

    MOV BX,[BP-8] ;cargo a bx con ref_ele

    MOV SI, [BP+6] ;apunto a M

    MOV AX,[SI+BX] ;M[ref_ele]

    IMUL [BP-16] ;M[ref_ele]*3

    ADD [BP-4],AX ;tdet+=M[ref_ele]*3

    ADD [BP-6],2 ;incremento a ref_ele (2 bytes)

    JMP for1

    endfor1:

    MOV AX,[BP-4] ;en ax devuelvo el det

    MOV SP,BP ;restauro la cima de la pila

    POP BP ;restauro el puntero base

    RET

    Bibliografía:

    Este trabajo se realizó con muy poca bibliografía. Se
    basó principalmente en las clases y consultas a
    profesores, papers correspondientes a la arquitectura de los
    procesadores Intel, y prácticas desarrolladas a modo de
    "prueba y error" en PC.

    Los únicos libros
    consultados son:

    • "Organización y Arquitectura de
      Computadores" William Stallings (4ta.
      Edición).
    • "Principios de
      la Arquitectura de Computadoras" Miles Murdocca & Vincent
      Heuring (1999).

    Agradecimientos:

    • A todos los docentes que me ayudaron para poder
      recopilar toda la información aquí
      contenida.
    • Intel Corporation, que me facilitó los papers
      de los procesadores que ya no fabrica hace muchos
      años.
    • AT&T, que tambíen me facilitó
      algunos papers sobre el funcionamiento de algunos algoritmos
      que aquí se utilizan.

    Diego Bellini

    Rosario – República Argentina

    Simplemente espero que les sea de mucha utilidad.

    Nota al lector: es posible que esta página no contenga todos los componentes del trabajo original (pies de página, avanzadas formulas matemáticas, esquemas o tablas complejas, etc.). Recuerde que para ver el trabajo en su versión original completa, puede descargarlo desde el menú superior.

    Todos los documentos disponibles en este sitio expresan los puntos de vista de sus respectivos autores y no de Monografias.com. El objetivo de Monografias.com es poner el conocimiento a disposición de toda su comunidad. Queda bajo la responsabilidad de cada lector el eventual uso que se le de a esta información. Asimismo, es obligatoria la cita del autor del contenido y de Monografias.com como fuentes de información.

    Categorias
    Newsletter