Um inteiro é uma abstração que representa números inteiros, como 4, -2, 0 ou 64532.
Para representar um inteiro como uma sequência de bytes, usa-se a notação binária. Nessa notação, cada bit da sequência representa uma potência de dois distinta, com o valor aumentando à medida que o índice do bit cresce da direita para a esquerda.
Se o número só puder ser não negativo, ele é chamado de número sem sinal.
Números sem sinal são representados diretamente como a soma das potências de dois correspondentes a todos os bits definidos em sua sequência.
O intervalo de inteiros não negativos representáveis em um registrador vai de 0 (nenhum bit definido) até 2⁶⁴ - 1 (soma de todos os 64 bits definidos).
Ampliar um número sem sinal para um tamanho maior é feito preenchendo todos os bits superiores com 0, de modo que nenhum bit novo contribua para o valor.
Isso é chamado de extensão de zeros.
A instrução movzx (z de zero) estende com zeros 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 todos os 64 bits do operando de destino com um simples mov.
Se um inteiro pode assumir valores positivos ou negativos, ele é chamado de número com sinal.
Para representar números negativos, o x86-64 usa a representação de complemento de dois.
No complemento de dois, os números com sinal também são representados como a soma das potências de dois correspondentes aos bits definidos. No entanto, se o bit mais alto estiver definido, ele é subtraído em vez de somado aos demais.
Como esse bit corresponde a um valor maior do que a soma de todos os outros, na prática isso significa que um número com esse bit definido é sempre negativo. Esse bit especial é chamado de bit de sinal.
Ampliar um número com sinal para um tamanho maior significa preencher cada novo bit superior com uma cópia do bit de sinal, para que o valor seja preservado. Isso é chamado de extensão de sinal.
A instrução movsx (s de sinal) estende o 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 mudar o sinal de um número.
Em assembly, não há como 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 uso de comentários pode ser de grande ajuda nessa tarefa.
Em um conceito anterior, foi mencionado que um número constante, como 4 ou -15, pode ser usado como operando de origem em muitas instruções.
Esses números são chamados de immediatos.
Um imediato não fica em um registrador nem na memória: ele é codificado dentro da própria instrução. Na maioria das instruções, o espaço reservado para ele tem apenas 32 bits de largura, não importa o tamanho do operando de destino.
Quando o operando de destino tem 64 bits de largura, esses 32 bits são estendidos com sinal para preenchê-lo. A metade superior do operando é totalmente preenchida com cópias do bit mais alto do imediato, então apenas um número dentro do intervalo de um inteiro com sinal de 32 bits pode ser escrito dessa forma:
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 essa regra é o mov, que pode receber um imediato completo de 64 bits quando o operando de destino é um registrador.
Se um imediato de 64 bits for necessário, primeiro use mov para carregá-lo em um registrador e depois use esse registrador:
mov rax, 3435973837 ; this works, mov can take a 64-bit immediate
mov rdx, 18446744073709551615 ; the largest immediate mov accepts
sub rdx, rax
Observe que um imediato negativo e o número sem sinal com a mesma representação de bits são equivalentes e montam exatamente o mesmo valor:
mov rax, -1 ; rax = 18446744073709551615
mov rax, 18446744073709551615 ; rax = -1
A adição de dois números pode ser calculada usando a instrução add.
Há também a instrução inc de um operando, que soma 1 ao valor em seu operando:
inc rax ; rax = rax + 1
A soma de dois inteiros funciona da mesma forma tanto para números sem sinal quanto com sinal.
A subtração de dois inteiros é realizada usando a instrução sub.
Há também a instrução dec de um operando, que subtrai 1 do valor em 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 quanto com sinal.
Existem duas instruções diferentes para realizar a multiplicação entre dois números no x86-64.
Como regra, 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
Dois registradores são usados implicitamente para realizar uma multiplicação na forma de um operando: rax e rdx.
Se a multiplicação envolve dois números de 64 bits, então os 64 bits inferiores do resultado ficarão em rax e os 64 bits superiores ficarão em rdx.
Isso geralmente é chamado de rdx:rax, para indicar que ambos os registradores são usados em conjunto:
mul rcx ; rax = lower 64 bits of rax * rcx
; rdx = upper 64 bits of rax * rcx
O mesmo acontece para outros tamanhos de operando.
Assim, por exemplo, se dois números de 32 bits estão sendo multiplicados, eax e edx serão usados.
A exceção é a multiplicação entre dois bytes.
Nesse caso, em vez de dl:al, será usado ax.
A parte inferior de ax (al) receberá os 8 bits inferiores do produto, enquanto a parte superior (ah) receberá os 8 bits superiores.
Os registradores usados implicitamente em uma multiplicação, como rax e rdx, são sempre sobrescritos.
Os valores nesses registradores devem ser salvos antes da operação se forem necessários depois.
A forma de dois operandos do imul tem um operando de destino explícito e segue a sintaxe usual.
rdx não é usado.
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, sendo o segundo sempre um imediato (um número constante).
Ambos os 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
Observe que o operando de destino não é usado na multiplicação. Ele apenas recebe o resultado.
Tanto a multiplicação com dois operandos quanto a com três operandos truncam o resultado para caber no tamanho do operando de destino.
A multiplicação com um operando preserva o intervalo completo, mas geralmente é dividida em dois registradores, rdx e rax.
Então, às vezes é útil ampliar os operandos antes da multiplicação para abrir espaço para o produto completo em um único registrador. 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
Como acontece com a multiplicação, também há duas instruções para realizar 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 com apenas um operando:
div src
idiv src
As divisões de 16, 32 e 64 bits usam dx:ax, edx:eax e rdx:rax como dividendo, respectivamente.
Nesses casos, ambos os registradores 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 é então 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 usar dl:al, usa-se ax.
Os 8 bits inferiores de ax (al) receberão o quociente da operação e os 8 bits superiores (ah) receberão o resto.
Observe que todos os bits do dividendo devem estar apropriadamente definidos antes da divisão.
Qualquer bit definido em rdx (ou em ah, no caso da divisão de 8 bits) contribui para o valor que está sendo dividido.
Na divisão sem sinal, quando o valor a ser dividido cabe na metade inferior, a metade superior deve ser zerada.
Qualquer instrução que zere esses bits serve.
Por exemplo, mov edx, 0 zera os bits superiores na divisão de 32 bits.
Na divisão com sinal, o valor deve ser estendido com sinal.
Existem instruções que automatizam esse processo: cbw, cwd, cdq e cqo.
A primeira define os bits em ah de acordo com o sinal de al.
As outras realizam a extensão de sinal de ax para dx, de eax para edx e de rax para rdx, respectivamente.
Os registradores usados implicitamente em uma divisão, como rax e rdx, são sempre sobrescritos.
Os valores nesses registradores devem ser salvos antes da divisão se forem necessários depois.
Uma loja local está mudando seu estoque para um galpão maior. Contrataram você para embalar e transportar tudo.
Você tem quatro tarefas, todas relacionadas a gerenciar o 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 |
Lembre-se de que você pode acessar o mesmo registrador com tamanhos diferentes mudando o nome do operando.
Por exemplo: rax (64 bits), eax (32 bits), ax (16 bits), al (8 bits).
Você pode consultar o conceito anterior para ver a tabela completa.
Os itens estão sendo embalados em caixas que precisam ser etiquetadas com o peso. Não há balança por perto, mas, por sorte, você sabe quanto cada item pesa em média.
Para organizar melhor as coisas, cada caixa guarda apenas itens de dois produtos diferentes.
Defina uma função get_box_weight que retorna o peso total de uma caixa, em g.
Essa função recebe como parâmetros, nesta ordem:
g
g
Considere que uma caixa vazia pesa 500 g.
Uma constante WEIGHT_OF_EMPTY_BOX está definida no topo do arquivo 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 de retorno é um inteiro não negativo de 32 bits.
As caixas são empilhadas e transportadas para o novo galpão em um caminhão. Porém, o espaço vertical no caminhão é limitado.
Defina uma função max_number_of_boxes que retorna quantas caixas de uma certa altura podem ser empilhadas verticalmente (uma em cima da outra) dentro do caminhão.
Essa função recebe como parâmetro a altura da caixa, em cm.
Considere que a altura interna do caminhão é 300 cm.
Uma constante TRUCK_HEIGHT está definida no topo do arquivo de solução.
Exemplo:
max_number_of_boxes(30);
// => 10
O argumento e o valor de retorno são inteiros não negativos de 8 bits.
A altura da caixa é sempre pelo menos 2, então o resultado cabe em 8 bits.
No novo galpão há um checklist com a quantidade de itens ainda não contabilizados de cada produto. Para cada nova caixa transportada para lá, você precisa calcular o novo valor no checklist para cada produto da caixa.
Defina uma função items_to_be_moved que retorna quantos itens ainda faltam mover para o novo galpão para um determinado produto.
Essa função recebe como parâmetros, nesta ordem:
Exemplo:
items_to_be_moved(76532, 120);
// => 76412
Os argumentos são inteiros não negativos de 32 bits. O valor de retorno é um inteiro de 32 bits. Em caso de erro no processo, é possível que o resultado seja um número negativo.
Seu pagamento é baseado em quantas caixas foram transportadas e quantas viagens de caminhão foram necessárias.
Por caixa, você receberá 5 dólares, e por viagem, receberá 220 dólares.
As constantes PAY_PER_BOX e PAY_PER_TRUCK_TRIP estão definidas no topo do arquivo de solução.
Repare que é possível que você tenha recebido parte desse pagamento adiantado para cobrir custos iniciais, e esse adiantamento deve ser subtraído do pagamento final. Além disso, alguns produtos não são cobertos por seguro, e seu pagamento também será reduzido pelo valor de quaisquer desses itens quebrados ou extraviados. É possível que você acabe devendo dinheiro se não tomar cuidado!
Isso significa que o valor líquido que você tem a receber, ou a pagar, é:
net = boxes * PAY_PER_BOX + trips * PAY_PER_TRUCK_TRIP - up_front - broken_items * item_value
Esse pagamento, ou dívida, será dividido igualmente entre você e um número de trabalhadores que você contratou.
Qualquer dinheiro, ou dívida, que sobrar é seu.
Por exemplo, se o valor líquido é 100, dividido entre 6 pessoas (você e 5 trabalhadores), você fica com 20 (100/(5 + 1) = 16 mais o resto 4).
Defina uma função calculate_payment que retorna quanto você deve receber, ou pagar, no final.
Essa função recebe como parâmetros, nesta ordem:
Exemplo:
calculate_payment(2000, 1000, 5, 21, 2, 1);
// => 2029
O valor de retorno é um inteiro de 64 bits.
Crie sua conta no Exercism para aprender e dominar x86-64 Assembly com 22 conceitos130 exercícios e mentoria humana de verdade, tudo de graça.