Um inteiro é uma abstração que representa números sem parte fracionária, como 4, -2, 0 ou 64532.
Para representar um inteiro como uma sequência de bytes, utiliza-se a notação binária. Nesta notação, cada bit da sequência representa uma potência de dois distinta, e o valor aumenta à medida que o índice do bit cresce da direita para a esquerda.
Se o número só puder ser não negativo, chama-se um número sem sinal.
Os números sem sinal são representados diretamente como a soma das potências de dois correspondentes a todos os bits a 1 da sua sequência.
O intervalo de inteiros não negativos representáveis num registro vai de 0 (nenhum bit a 1) a 2⁶⁴ - 1 (soma de todos os 64 bits a 1).
Alargar um número sem sinal para um tamanho maior faz-se preenchendo todos os bits superiores com 0, para que nenhum bit novo contribua para o valor.
A isto chama-se extensão de zeros.
A instrução movzx (z de zero) faz a extensão de zeros de um operando de origem de 8 ou 16 bits para um operando de destino maior.
Um operando de origem de 32 bits é sempre estendido com zeros para os 64 bits do operando de destino com um simples mov.
Se um inteiro puder assumir valores positivos ou negativos, chama-se um número com sinal.
Para representar números negativos, o x86-64 usa a representação em complemento para dois.
Em complemento para dois, os números com sinal também são representados como a soma das potências de dois correspondentes aos bits a 1. No entanto, se o bit mais alto estiver a 1, é subtraído em vez de somado aos restantes.
Como este bit corresponde a um valor superior à soma de todos os outros, na prática isto significa que um número com este bit a 1 é sempre negativo. A este bit especial chama-se bit de sinal.
Alargar um número com sinal para um tamanho maior significa preencher todos os bits superiores novos com uma cópia do bit de sinal, para que o valor se preserve. A isto chama-se extensão de sinal.
A instrução movsx (s de sinal) faz a extensão de sinal de um operando de origem de 8 ou 16 bits para um operando de destino maior.
Uma variante de movsx chamada movsxd faz o mesmo de um operando de origem de 32 bits para um operando de destino de 64 bits.
A instrução neg pode ser usada para alterar o sinal de um número.
Em assembly, não há forma de saber se uma sequência de bytes representa um número com sinal ou sem sinal. É responsabilidade do programador dar significado a esses bytes.
O recurso a comentários pode ser uma grande ajuda nesta tarefa.
Num conceito anterior, foi mencionado que um número constante, como 4 ou -15, pode ser usado como operando de origem em muitas instruções.
A esses números chama-se imediato.
Um imediato não fica guardado num registro nem na memória: está codificado na própria instrução. Na maioria das instruções, o espaço reservado para ele tem apenas 32 bits, independentemente do tamanho do operando de destino.
Quando o operando de destino tem 64 bits, esses 32 bits são estendidos com o sinal para o preencher. A metade superior do operando fica totalmente preenchida com cópias do bit mais alto do imediato, por isso só se pode escrever desta forma um número dentro do intervalo de um inteiro com sinal 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
Um número fora desse intervalo não pode ser usado como imediato.
A exceção a esta regra é o mov, que pode receber um imediato completo de 64 bits quando o operando de destino é um registro.
Se for preciso um imediato de 64 bits, usa primeiro o mov para o carregar para um registro e depois usa esse registro:
mov rax, 3435973837 ; this works, mov can take a 64-bit immediate
mov rdx, 18446744073709551615 ; the largest immediate mov accepts
sub rdx, rax
Repara que um imediato negativo e o número sem sinal com a mesma representação binária são equivalentes e originam exatamente o mesmo valor depois de montados:
mov rax, -1 ; rax = 18446744073709551615
mov rax, 18446744073709551615 ; rax = -1
A adição de dois números pode ser calculada com a instrução add.
Existe também a instrução inc, de um operando, que soma 1 ao valor do seu operando:
inc rax ; rax = rax + 1
A soma de dois inteiros funciona da mesma forma tanto para números sem sinal como com sinal.
A subtração de dois inteiros efetua-se com a instrução sub.
Existe também a instrução dec, de um operando, que subtrai 1 ao valor do seu operando:
dec rax ; rax = rax - 1
A subtração de dois inteiros também funciona da mesma forma tanto para números sem sinal como com sinal.
Há duas instruções diferentes para efetuar a multiplicação entre dois números em x86-64.
Regra geral, a multiplicação sem sinal usa a instrução mul, enquanto a multiplicação com sinal usa imul.
A instrução mul assume a seguinte forma de um operando, em que src é o operando de origem:
mul src
A instrução imul pode assumir a forma de um, dois ou três operandos:
imul src
imul dest, src
imul dest, src1, src2
Na forma de um operando, são usados implicitamente dois registros para efetuar a multiplicação: rax e rdx.
Se a multiplicação envolver dois números de 64 bits, os 64 bits inferiores do resultado ficam em rax e os 64 bits superiores ficam em rdx.
A isto chama-se habitualmente rdx:rax, para indicar que os dois registros são usados em conjunto:
mul rcx ; rax = lower 64 bits of rax * rcx
; rdx = upper 64 bits of rax * rcx
O mesmo acontece com outros tamanhos de operandos.
Por exemplo, se se estiverem a multiplicar dois números de 32 bits, serão usados eax e edx.
A exceção é a multiplicação entre dois bytes.
Neste caso, em vez de dl:al, usa-se ax.
A parte inferior de ax (al) recebe os 8 bits inferiores do produto, enquanto a parte superior (ah) recebe os 8 bits superiores.
Os registros usados implicitamente numa multiplicação, como rax e rdx, são sempre substituídos.
Se forem necessários mais tarde, os valores desses registros devem ser guardados antes da operação.
A forma de dois operandos do imul tem um operando de destino explícito e segue a sintaxe habitual.
Não se usa o rdx.
Em vez disso, o resultado é truncado para caber no operando de destino.
imul r8, r9 ; r8 = lower 64 bits of r8 * r9
A forma de três operandos do imul tem dois operandos de origem, o segundo dos quais é sempre um imediato (um número constante).
Os dois operandos de origem são multiplicados e o resultado é truncado e colocado no operando de destino:
imul r8, r9, 100 ; r8 = lower 64 bits of r9 * 100
Repara que o operando de destino não é usado na multiplicação. Apenas recebe o resultado.
Tanto a multiplicação de dois operandos como a de três operandos truncam o resultado para caber no tamanho do operando de destino.
A multiplicação de um operando preserva o intervalo completo, mas é habitualmente dividida por dois registros, rdx e rax.
Por isso, às vezes é útil alargar os operandos antes da multiplicação para abrir espaço para o produto completo num único registro. Um operando sem sinal é estendido com zeros, enquanto um com sinal é estendido com sinal:
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
Tal como na multiplicação, também há duas instruções para efetuar a divisão entre dois números.
A divisão sem sinal usa a instrução div, enquanto a divisão com sinal usa idiv.
Ambas as instruções funcionam apenas com um operando:
div src
idiv src
A divisão de 16, 32 e 64 bits usa dx:ax, edx:eax e rdx:rax como dividendo, respetivamente.
Nestes casos, os dois registros atuam em conjunto para criar um valor de 2N bits, em que N é o tamanho da operação (16, 32 ou 64 bits).
Esse valor é depois dividido pelo operando de origem.
O quociente é escrito em ax, eax ou rax e o resto é escrito em dx, edx ou rdx, de acordo com o tamanho da operação.
A divisão entre bytes é especial: em vez de se usar dl:al, usa-se ax.
Os 8 bits inferiores de ax (al) recebem o quociente da operação e os 8 bits superiores (ah) recebem o resto.
Repara que todos os bits do dividendo devem ser devidamente preparados antes da divisão.
Qualquer bit a 1 em rdx (ou em ah, no caso da divisão de 8 bits) contribui para o valor que está a ser dividido.
Na divisão sem sinal, quando o valor a dividir cabe na metade inferior, a metade superior deve ser limpa.
Serve qualquer instrução que limpe esses bits.
Por exemplo, mov edx, 0 limpa os bits superiores na divisão de 32 bits.
Na divisão com sinal, o valor deve, em vez disso, ser estendido com sinal.
Há instruções que automatizam este processo: cbw, cwd, cdq e cqo.
A primeira coloca os bits em ah de acordo com o sinal de al.
As outras fazem a extensão de sinal de ax para dx, de eax para edx e de rax para rdx, respetivamente.
Os registros usados implicitamente numa divisão, como rax e rdx, são sempre substituídos.
Se forem necessários mais tarde, os valores desses registros devem ser guardados antes da divisão.
Uma loja local está a mudar o seu inventário para um armazém maior. Foste contratado para embalar e transportar tudo.
Tens quatro tarefas, todas relacionadas com a gestão do transporte.
Estas são as instruções mencionadas neste conceito:
| Instrução | Descrição |
|---|---|
| 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 |
Lembra-te de que podes aceder ao mesmo registo com tamanhos diferentes, mudando o nome do operando.
Por exemplo: rax (64 bits), eax (32 bits), ax (16 bits), al (8 bits).
Podes consultar o conceito anterior para a tabela completa.
Os itens estão a ser embalados em caixas que têm de ser etiquetadas com o seu peso. Não há nenhuma balança por perto, mas, felizmente, sabes quanto pesa cada item, em média.
Para organizar melhor as coisas, cada caixa contém apenas itens de dois produtos diferentes.
Define uma função get_box_weight que devolve o peso total de uma caixa, em g.
Esta função recebe como parâmetros, por esta ordem:
g
g
Considera que uma caixa vazia pesa 500 g.
A constante WEIGHT_OF_EMPTY_BOX está definida no topo do ficheiro de solução.
Exemplo:
get_box_weight(30, 40, 50, 20);
// => 2700
Todos os argumentos são inteiros não negativos de 16 bits, e o valor devolvido é um inteiro não negativo de 32 bits.
As caixas estão a ser empilhadas e transportadas para o novo armazém num camião. No entanto, o espaço vertical no camião é limitado.
Define uma função max_number_of_boxes que devolve quantas caixas de uma determinada altura podem ser empilhadas verticalmente (uma em cima da outra) dentro do camião.
Esta função recebe como parâmetro a altura da caixa, em cm.
Considera que a altura interior do camião é 300 cm.
A constante TRUCK_HEIGHT está definida no topo do ficheiro de solução.
Exemplo:
max_number_of_boxes(30);
// => 10
O argumento e o valor devolvido são inteiros não negativos de 8 bits.
A altura da caixa é sempre pelo menos 2, por isso o resultado cabe em 8 bits.
Há uma lista de verificação no novo armazém com o número de itens ainda por contabilizar para cada produto. Para cada nova caixa transportada para lá, tens de calcular o novo valor na lista de verificação para cada produto que esteja na caixa.
Define uma função items_to_be_moved que devolve quantos itens faltam transportar para o novo armazém, para um determinado produto.
Esta função recebe como parâmetros, por esta ordem:
Exemplo:
items_to_be_moved(76532, 120);
// => 76412
Os argumentos são inteiros não negativos de 32 bits. O valor devolvido é um inteiro de 32 bits. Em caso de erro no processo, é possível que o resultado seja um número negativo.
O teu pagamento depende de quantas caixas foram transportadas e de quantas viagens de camião foram necessárias.
Por cada caixa, recebes 5 dólares, e por cada viagem, recebes 220 dólares.
As constantes PAY_PER_BOX e PAY_PER_TRUCK_TRIP estão definidas no topo do ficheiro de solução.
Repara que podes ter recebido parte deste pagamento adiantado para cobrir custos iniciais, e esse pagamento adiantado deve ser subtraído ao pagamento final. Além disso, alguns produtos não estão cobertos por seguro, e o teu pagamento será também reduzido pelo valor dos itens desses produtos que estejam partidos ou em falta. É possível que acabes por ficar a dever dinheiro, se não tiveres cuidado!
Isto significa que o valor líquido que tens a receber, ou a pagar, é:
net = boxes * PAY_PER_BOX + trips * PAY_PER_TRUCK_TRIP - up_front - broken_items * item_value
Este pagamento, ou dívida, será dividido igualmente entre ti e um certo número de trabalhadores que contrataste.
O dinheiro que sobrar, ou a dívida, é teu.
Por exemplo, se o valor líquido for 100, a partilhar entre 6 pessoas (tu e 5 trabalhadores), recebes 20 (100/(5 + 1) = 16 mais os 4 restantes).
Define uma função calculate_payment que devolve quanto deves receber, ou pagar, no final.
Esta função recebe como parâmetros, por esta ordem:
Exemplo:
calculate_payment(2000, 1000, 5, 21, 2, 1);
// => 2029
O valor devolvido é um inteiro de 64 bits.
Inscreve-te no Exercism para aprenderes e dominares x86-64 Assembly com 22 conceitos130 exercícios, e mentoria humana real, tudo grátis.