Un número entero es una abstracción que representa números sin parte fraccionaria, como 4, -2, 0 o 64532.
Para representar un número entero como una secuencia de bytes, se utiliza la notación binaria. En esta notación, cada bit de la secuencia representa una potencia de dos distinta, y el valor aumenta a medida que lo hace el índice del bit de derecha a izquierda.
Si el número solo puede ser no negativo, se denomina número sin signo.
Los números sin signo se representan directamente como la suma de las potencias de dos correspondientes a todos los bits activados de su secuencia.
El rango de números enteros no negativos representables en un registro va desde 0 (ningún bit activado) hasta 2⁶⁴ - 1 (la suma de los 64 bits activados).
Ensanchar un número sin signo a un tamaño mayor se hace rellenando todos los bits superiores con 0, de modo que ningún bit nuevo contribuya al valor.
Esto se denomina extensión de ceros.
La instrucción movzx (z de cero) extiende con ceros un operando origen de 8 o 16 bits a un operando destino de mayor tamaño.
Un operando origen de 32 bits siempre se extiende con ceros a los 64 bits del operando destino con un simple mov.
Si un número entero puede tomar valores positivos o negativos, se denomina número con signo.
Para representar números negativos, x86-64 utiliza la representación en complemento a dos.
En complemento a dos, los números con signo también se representan como la suma de las potencias de dos correspondientes a los bits activados. Sin embargo, si el bit más alto está activado, se resta en lugar de sumarse a los demás.
Como este bit corresponde a un valor mayor que la suma de todos los demás, en la práctica esto significa que un número con este bit activado siempre es negativo. Este bit especial se denomina bit de signo.
Ensanchar un número con signo a un tamaño mayor consiste en rellenar cada nuevo bit superior con una copia del bit de signo, de modo que se conserve el valor. Esto se denomina extensión de signo.
La instrucción movsx (s de signo) extiende el signo de un operando origen de 8 o 16 bits a un operando destino de mayor tamaño.
Una variante de movsx llamada movsxd hace lo mismo desde un operando origen de 32 bits a un operando destino de 64 bits.
La instrucción neg se puede usar para cambiar el signo de un número.
En ensamblador, no hay forma de saber si una secuencia de bytes representa un número con signo o uno sin signo. Es responsabilidad de quien programa dar significado a esos bytes.
El uso de comentarios puede ser de gran ayuda en esta tarea.
En un concepto anterior se mencionó que un número constante, como 4 o -15, puede usarse como operando origen de muchas instrucciones.
Esos números se denominan inmediatos.
Un inmediato no se guarda en un registro ni en memoria: está codificado dentro de la propia instrucción. En la mayoría de las instrucciones, el espacio reservado para él es de solo 32 bits, sin importar el tamaño del operando destino.
Cuando el operando destino es de 64 bits, esos 32 bits se extienden con signo para rellenarlo. La mitad superior del operando se rellena por completo con copias del bit más alto del inmediato, por lo que solo se puede escribir de esta forma un número dentro del rango de un entero con signo de 32 bits:
add rax, -1 ; the immediate is sign-extended, so all 64 bits of rax are affected
add rax, 2147483647 ; the largest immediate an instruction like this accepts
Un número fuera de ese rango no puede usarse como inmediato.
La excepción a esta regla es mov, que puede tomar un inmediato completo de 64 bits cuando el operando destino es un registro.
Si se necesita un inmediato de 64 bits, usa primero mov para cargarlo en un registro y luego usa ese registro:
mov rax, 3435973837 ; this works, mov can take a 64-bit immediate
mov rdx, 18446744073709551615 ; the largest immediate mov accepts
sub rdx, rax
Ten en cuenta que un inmediato negativo y el número sin signo con la misma representación de bits son equivalentes y se ensamblan exactamente al mismo valor:
mov rax, -1 ; rax = 18446744073709551615
mov rax, 18446744073709551615 ; rax = -1
La suma de dos números se puede calcular con la instrucción add.
También existe la instrucción inc, de un solo operando, que suma 1 al valor de su operando:
inc rax ; rax = rax + 1
La suma de dos números enteros funciona igual tanto para números sin signo como con signo.
La resta de dos números enteros se realiza con la instrucción sub.
También existe la instrucción dec, de un solo operando, que resta 1 al valor de su operando:
dec rax ; rax = rax - 1
La resta de dos números enteros también funciona igual tanto para números sin signo como con signo.
Hay dos instrucciones distintas para multiplicar dos números en x86-64.
Por norma general, la multiplicación sin signo usa la instrucción mul, mientras que la multiplicación con signo usa imul.
La instrucción mul adopta la siguiente forma de un solo operando, donde src es el operando origen:
mul src
La instrucción imul puede adoptar la forma de uno, dos o tres operandos:
imul src
imul dest, src
imul dest, src1, src2
En la forma de un solo operando se usan implícitamente dos registros para realizar la multiplicación: rax y rdx.
Si la multiplicación involucra dos números de 64 bits, los 64 bits inferiores del resultado estarán en rax y los 64 bits superiores estarán en rdx.
Esto se suele denominar rdx:rax, para indicar que ambos registros se usan en conjunto:
mul rcx ; rax = lower 64 bits of rax * rcx
; rdx = upper 64 bits of rax * rcx
Lo mismo ocurre con otros tamaños de operando.
Así, por ejemplo, si se multiplican dos números de 32 bits, se usarán eax y edx.
La excepción es la multiplicación entre dos bytes.
En este caso, en lugar de dl:al, se usará ax.
La parte inferior de ax (al) recibirá los 8 bits inferiores del producto, mientras que la parte superior (ah) recibirá los 8 bits superiores.
Los registros que se usan implícitamente en una multiplicación, como rax y rdx, siempre se sobrescriben.
Los valores de esos registros deben guardarse antes de la operación si se van a necesitar después.
La forma de dos operandos de imul tiene un operando destino explícito y sigue la sintaxis habitual.
No se usa rdx.
En su lugar, el resultado se trunca para que quepa en el operando destino.
imul r8, r9 ; r8 = lower 64 bits of r8 * r9
La forma de tres operandos de imul tiene dos operandos origen, el segundo de los cuales es siempre un inmediato (un número constante).
Se multiplican ambos operandos origen y el resultado se trunca y se coloca en el operando destino:
imul r8, r9, 100 ; r8 = lower 64 bits of r9 * 100
Ten en cuenta que el operando destino no se usa en la multiplicación. Solo recibe el resultado.
Tanto la multiplicación de dos operandos como la de tres operandos truncan el resultado para que quepa en el tamaño del operando destino.
La multiplicación de un solo operando conserva el rango completo, pero normalmente se reparte entre dos registros, rdx y rax.
Por eso, a veces es útil ensanchar los operandos antes de la multiplicación para hacer sitio al producto completo en un solo registro. Un operando sin signo se extiende con ceros, mientras que uno con signo se extiende con el signo:
movzx eax, di ; di and si hold unsigned 16-bit numbers
movzx ecx, si
mul ecx ; the 32-bit product fits in eax, and edx is cleared
Al igual que ocurre con la multiplicación, también hay dos instrucciones para dividir dos números.
La división sin signo usa la instrucción div, mientras que la división con signo usa idiv.
Ambas instrucciones funcionan con un solo operando:
div src
idiv src
La división de 16, 32 y 64 bits usa dx:ax, edx:eax y rdx:rax como dividendo, respectivamente.
En estos casos, ambos registros actúan en conjunto para crear un valor de 2N bits, donde N es el tamaño de la operación (16, 32 o 64 bits).
Este valor se divide entonces entre el operando origen.
El cociente se escribe en ax, eax o rax y el residuo se escribe en dx, edx o rdx, según el tamaño de la operación.
La división entre bytes es especial: en lugar de usar dl:al, se usa ax.
Los 8 bits inferiores de ax (al) recibirán el cociente de la operación y los 8 bits superiores (ah) recibirán el residuo.
Ten en cuenta que todos los bits del dividendo deben estar debidamente establecidos antes de la división.
Cualquier bit activado en rdx (o en ah para la división de 8 bits) contribuye al valor que se divide.
En la división sin signo, cuando el valor que se va a dividir cabe en la mitad inferior, la mitad superior debe ponerse a cero.
Sirve cualquier instrucción que ponga esos bits a cero.
Por ejemplo, mov edx, 0 pone a cero los bits superiores en la división de 32 bits.
En la división con signo, en cambio, el valor debe extenderse con el signo.
Hay instrucciones que automatizan este proceso: cbw, cwd, cdq y cqo.
La primera establece los bits de ah según el signo de al.
Las demás realizan la extensión de signo de ax a dx, de eax a edx y de rax a rdx, respectivamente.
Los registros que se usan implícitamente en una división, como rax y rdx, siempre se sobrescriben.
Los valores de esos registros deben guardarse antes de la división si se van a necesitar después.
Una tienda local está trasladando su inventario a un almacén más grande. Te han contratado para empaquetar y trasladarlo todo.
Tienes cuatro tareas, todas relacionadas con la gestión del transporte.
Estas son las instrucciones mencionadas en este concepto:
| Instrucción | Descripción |
|---|---|
| add a, b | a = a + b |
| inc a | a = a + 1 |
| sub a, b | a = a - b |
| dec a | a = a - 1 |
| imul a | rdx:rax = a * rax (con signo) |
| imul a, b | a = a * b (con signo, truncado) |
| imul a, b, c | a = b * c (con signo, truncado) |
| mul a | rdx:rax = a * rax (sin signo) |
| div a | rax = cociente, rdx = residuo de rdx:rax / a (sin signo) |
| idiv a | rax = cociente, rdx = residuo de rdx:rax / a (con signo) |
| movzx a, b | a = b, añadiendo 0 a los bits adicionales |
| movsx a, b | a = b, añadiendo 1 a los bits adicionales si b < 0 o 0 en caso contrario |
Recuerda que puedes acceder al mismo registro con distintos tamaños cambiando el nombre del operando.
Por ejemplo: rax (64 bits), eax (32 bits), ax (16 bits), al (8 bits).
Puedes consultar el concepto anterior para ver la tabla completa.
Los artículos se empaquetan en cajas que deben etiquetarse con su peso. No hay ninguna báscula a mano, pero por suerte sabes cuánto pesa de media cada artículo.
Para organizarlo mejor, cada caja solo contiene artículos de dos productos distintos.
Define una función get_box_weight que devuelva el peso total de una caja, en g.
Esta función recibe como parámetros, en este orden:
g
g
Ten en cuenta que una caja vacía pesa 500 g.
Al principio del archivo de solución se define la constante WEIGHT_OF_EMPTY_BOX.
Ejemplo:
get_box_weight(30, 40, 50, 20);
// => 2700
Todos los argumentos son enteros no negativos de 16 bits, y el valor devuelto es un entero no negativo de 32 bits.
Las cajas se apilan y se trasladan al nuevo almacén en un camión. Sin embargo, el espacio vertical del camión es limitado.
Define una función max_number_of_boxes que devuelva cuántas cajas de una altura determinada se pueden apilar verticalmente (una encima de otra) dentro del camión.
Esta función recibe como parámetro la altura de la caja, en cm.
Ten en cuenta que la altura interior del camión es de 300 cm.
Al principio del archivo de solución se define la constante TRUCK_HEIGHT.
Ejemplo:
max_number_of_boxes(30);
// => 10
El argumento y el valor devuelto son enteros no negativos de 8 bits.
La altura de la caja siempre es como mínimo 2, así que el resultado cabe en 8 bits.
En el nuevo almacén hay una lista de comprobación con el número de artículos que aún quedan pendientes de contabilizar de cada producto. Por cada caja nueva que se traslade allí, tienes que calcular el nuevo valor de la lista de comprobación para cada producto que haya en la caja.
Define una función items_to_be_moved que devuelva cuántos artículos quedan por trasladar al nuevo almacén para un producto dado.
Esta función recibe como parámetros, en este orden:
Ejemplo:
items_to_be_moved(76532, 120);
// => 76412
Los argumentos son enteros no negativos de 32 bits. El valor devuelto es un entero de 32 bits. Si se produce un error en el proceso, es posible que el resultado sea un número negativo.
Tu pago depende de cuántas cajas se hayan trasladado y de cuántos viajes en camión hayan sido necesarios.
Por cada caja te pagarán 5 dólares y por cada viaje te pagarán 220 dólares.
Al principio del archivo de solución se definen las constantes PAY_PER_BOX y PAY_PER_TRUCK_TRIP.
Ten en cuenta que puede que hayas recibido por adelantado parte de este pago para cubrir los costes iniciales, y ese pago por adelantado debe restarse del pago final. Además, algunos productos no están cubiertos por el seguro y tu pago también se reducirá por el valor de cualquiera de esos artículos que se rompa o desaparezca. Es posible que acabes debiendo dinero si no tienes cuidado.
Esto significa que la cantidad neta que te deben, o que debes, es:
net = boxes * PAY_PER_BOX + trips * PAY_PER_TRUCK_TRIP - up_front - broken_items * item_value
Este pago, o esta deuda, se repartirá a partes iguales entre tú y un número de trabajadores que hayas contratado.
Cualquier dinero restante, o deuda, es tuyo.
Por ejemplo, si la cantidad neta de dinero es 100 y se reparte entre 6 personas (tú y 5 trabajadores), tú recibes 20 (100/(5 + 1) = 16 más los 4 que sobran).
Define una función calculate_payment que devuelva cuánto deberían pagarte, o cuánto debes pagar tú, al final.
Esta función recibe como parámetros, en este orden:
Ejemplo:
calculate_payment(2000, 1000, 5, 21, 2, 1);
// => 2029
El valor devuelto es un entero de 64 bits.
Regístrate en Exercism para aprender y dominar x86-64 Assembly con 22 conceptos130 ejercicios y mentoría humana real, todo gratis.