Un número entero es una abstracción que representa números completos, como 4, -2, 0 o 64532.
Para representar un número entero como una secuencia de bytes, se usa 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 el índice del bit crece de derecha a izquierda.
Si el número solo puede ser no negativo, se dice que es un 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 establecidos en su secuencia.
El rango de números enteros no negativos representables en un registro va de 0 (ningún bit establecido) a 2⁶⁴ - 1 (la suma de los 64 bits establecidos).
Para ampliar un número sin signo a un tamaño mayor, se rellenan todos los bits superiores con 0, de modo que ningún bit nuevo contribuya al valor.
Esto se llama extensión con ceros.
La instrucción movzx (la z de zero) extiende con ceros un operando fuente de 8 o 16 bits a un operando destino más grande.
Un operando fuente 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 dice que es un número con signo.
Para representar números negativos, x86-64 usa 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 establecidos. Sin embargo, si el bit más alto está establecido, 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 establecido siempre es negativo. Este bit especial se llama bit de signo.
Ampliar un número con signo a un tamaño mayor significa rellenar cada bit superior nuevo con una copia del bit de signo, para que el valor se conserve. Esto se llama extensión de signo.
La instrucción movsx (la s de sign) extiende el signo de un operando fuente de 8 o 16 bits a un operando destino más grande.
Una variante de movsx llamada movsxd hace lo mismo desde un operando fuente de 32 bits a un operando destino de 64 bits.
La instrucción neg puede usarse 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 sin signo. Es responsabilidad de quien programa darles 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 fuente en muchas instrucciones.
Esos números se llaman inmediatos.
Un inmediato no se guarda en un registro ni en memoria: se codifica 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 lo grande que sea el operando destino.
Cuando el operando destino es de 64 bits, esos 32 bits se extienden con el signo para rellenarlo. La mitad superior del operando se rellena por completo con copias del bit más alto del inmediato, así 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 necesitas un inmediato de 64 bits, primero usa mov para cargarlo en un registro y después 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 de la misma manera 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 de la misma manera tanto para números sin signo como con signo.
Hay dos instrucciones distintas para multiplicar dos números en x86-64.
Por regla general, la multiplicación sin signo usa la instrucción mul, mientras que la multiplicación con signo usa imul.
La instrucción mul toma la siguiente forma de un operando, donde src es el operando fuente:
mul src
La instrucción imul puede tomar la forma de uno, dos o tres operandos:
imul src
imul dest, src
imul dest, src1, src2
En la multiplicación con un operando se usan implícitamente dos registros: rax y rdx.
Si la multiplicación involucra dos números de 64 bits, los 64 bits inferiores del resultado quedan en rax y los 64 bits superiores en rdx.
Esto suele escribirse como 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.
Si los valores de esos registros se necesitan más adelante, guárdalos antes de la operación.
La forma de dos operandos de imul tiene un operando destino explícito y sigue la sintaxis habitual.
rdx no se usa.
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 fuente, y el segundo es siempre un inmediato (un número constante).
Ambos operandos fuente se multiplican, 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 con dos operandos como la de tres operandos truncan el resultado para que quepa en el tamaño del operando destino.
La multiplicación con un operando conserva el rango completo, pero por lo general se reparte entre dos registros, rdx y rax.
Por eso, a veces es útil ampliar los operandos antes de la multiplicación para dejar espacio 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
Igual que 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 formar un valor de 2N bits, donde N es el tamaño de la operación (16, 32 o 64 bits).
Ese valor se divide después por el operando fuente.
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 establecidos de forma adecuada antes de la división.
Cualquier bit establecido 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, hay que limpiar la mitad superior.
Sirve cualquier instrucción que limpie esos bits.
Por ejemplo, mov edx, 0 limpia 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 otras 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.
Si los valores de esos registros se necesitan más adelante, guárdalos antes de la división.
Una tienda local está mudando su inventario a un almacén más grande. Te contrataron para empacar y mover todo.
Tienes cuatro tareas, todas relacionadas con gestionar el 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 (signed) |
| imul a, b | a = a * b (signed, truncated) |
| imul a, b, c | a = b * c (signed, truncated) |
| mul a | rdx:rax = a * rax (unsigned) |
| div a | rax = quotient, rdx = remainder of rdx:rax / a (unsigned) |
| idiv a | rax = quotient, rdx = remainder of rdx:rax / a (signed) |
| movzx a, b | a = b, adding 0 to the extra bits |
| movsx a, b | a = b, adding 1 to the extra bits if b < 0 or 0 otherwise |
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 están empacando en cajas que deben etiquetarse con su peso. No hay ninguna balanza cerca, pero por suerte sabes cuánto pesa cada artículo en promedio.
Para organizar mejor las cosas, cada caja contiene solo artículos de dos productos diferentes.
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.
La constante WEIGHT_OF_EMPTY_BOX está definida al inicio del archivo de solución.
Ejemplo:
get_box_weight(30, 40, 50, 20);
// => 2700
Todos los argumentos son enteros de 16 bits no negativos, y el valor de retorno es un entero de 32 bits no negativo.
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 cierta altura 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.
La constante TRUCK_HEIGHT está definida al inicio del archivo de solución.
Ejemplo:
max_number_of_boxes(30);
// => 10
El argumento y el valor de retorno son enteros de 8 bits no negativos.
La altura de la caja siempre es al menos 2, así que el resultado cabe en 8 bits.
En el nuevo almacén hay una lista de verificación con la cantidad de artículos que todavía no se han contabilizado de cada producto. Por cada caja nueva que se mueve allí, necesitas calcular el nuevo valor en la lista de verificación para cada producto de la caja.
Define una función items_to_be_moved que devuelva cuántos artículos quedan por mover 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 de 32 bits no negativos. El valor de retorno es un entero de 32 bits. Si ocurre un error en el proceso, es posible que el resultado sea un número negativo.
Tu pago depende de cuántas cajas se movieron y cuántos viajes en camión fueron necesarios.
Por cada caja te pagarán 5 dólares y por cada viaje te pagarán 220 dólares.
Las constantes PAY_PER_BOX y PAY_PER_TRUCK_TRIP están definidas al inicio del archivo de solución.
Ten en cuenta que es posible que hayas recibido parte de este pago por adelantado para cubrir los costos 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 falte. ¡Es posible que termines debiendo dinero si no tienes cuidado!
Esto significa que el monto neto 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 deuda, se repartirá en partes iguales entre tú y los trabajadores que hayas contratado.
El dinero restante, o la deuda restante, es tuyo.
Por ejemplo, si el monto neto de dinero es 100 y se reparte entre 6 personas (tú y 5 trabajadores), te quedas con 20 (100/(5 + 1) = 16 más los 4 restantes).
Define una función calculate_payment que devuelva cuánto te deben pagar, o cuánto debes pagar, al final.
Esta función recibe como parámetros, en este orden:
Ejemplo:
calculate_payment(2000, 1000, 5, 21, 2, 1);
// => 2029
El valor de retorno 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.