In this chapter, you’ll see a number of arithmetic instructions for integers. Floating-point arithmetic will be covered in a later chapter. Now is a good time to quickly review Chapter 2 on binary numbers.
Starting with Integer Arithmetic
icalc.asm
Examining Arithmetic Instructions
Many arithmetic instructions are available; we are going to show a selection of them, and the others are similar to what you’ll learn here. Before we investigate the arithmetic instructions, note that we use printf with more than two arguments, so we need an additional register: the first argument goes into rdi, the second into rsi, and the third into rdx. That is how printf expects us to provide the arguments in Linux. You’ll learn more about that later, when we talk about calling conventions.
The first instruction is add , which can be used to add signed or unsigned integers. The second operand (source) is added to the first operand (destination), and the result is placed in the first operand (destination). The destination operand can be a register or a memory location. The source can be an immediate value, a register, or a memory location. The source and destination cannot be a memory location in the same instruction. When the resulting sum is too large to fit in the destination, the CF flag is set for signed integers. For unsigned integers, the OF flag is then set. When the result is 0, the ZF flag is set to 1, and when the result is negative, the SF flag is set.
The subtraction with sub is similar to the add instruction.
To increment a register or value in a memory location with 1, use the inc instruction . Similarly, dec can be used to decrement a register or value in a memory location with 1.
The arithmetic shift instructions are a special breed. The shift left, sal , is in fact multiplying; if you shift left one position, you are multiplying by 2. Every bit is shifted one place to the left, and a 0 is added to the right. Take the binary number 1. Shift left one place, and you obtain binary 10 or 2 in decimal representation. Shift left one place again, and you have binary 100 or 4 in decimal representation. If you shift left two positions, you multiply by 4. What if you want to multiply by 6? You shift left two times and then the add two times the original source, in that order.
Shift right, sar , is similar to shift left, but it means dividing by 2. Every bit is shifted one place to the right, and an additional bit is added to the left. Here there is a complication, however: if the original value was negative, the leftmost bit would be 1; if the shift instruction added a 0 bit at the left, the value would become positive, and the result would be wrong. So, in the case of a negative value, a sar will add a 1 bit to the left, and in the case of a positive value, 0 bits will be added to the left. This is called sign extension. By the way, a quick way to see if a hexadecimal number is negative is to look at byte 7 (the leftmost byte, counting from byte 0, which is the rightmost byte). The number is negative if byte 7 starts with an 8, 9, A, B, C, D, E, or F. But you need to take into account all 8 bytes. For example, 0xd12 is still a positive number because the leftmost byte, which is not shown, is a 0.
There are also nonarithmetic shift instructions; they will be discussed in Chapter 16.
Next, we multiply integers. For multiplying unsigned integers, you can use mul for unsigned multiplication and imul for signed multiplication. We will use imul, signed multiplication, which offers more flexibility: imul can take one, two, or three operands. In our example, we use one operand; the operand following the imul instruction is multiplied with the value in rax. You may expect that the resulting product is stored in rax, but that is not entirely correct. Let’s illustrate with an example: you can verify that when you multiply, for example, a two-digit number with a three-digit number, the product has four or five digits. When you multiply a 48-bit digit with a 30-bit digit, you will obtain a 77-bit digit or a 78-bit digit, and that value does not fit in a 64-bit register. To cope with this, the instruction imul will store the lower 64 bits of the resulting product in rax and the upper 64 bits in rdx. And this can be very deceptive!
Let’s experiment a little bit: go back to the source code in SASM. Modify number1 so that it contains 12345678901234567 and modify number2 so that it contains 100. The product will just fit in rax; you can check that in SASM debug mode. Put a break before the imul instruction. Restart debugging mode and step through the program. The result of the multiplication will be 1234567890123456700, as you can see in rax after the imul instruction is executed. Now modify number2 into 10000. Restart debugging. Look at rax after executing imul. You see that the product is a large negative number! That is because the most significant bit in rax is a 1 and SASM concludes that this must be a negative number. Also, printf thinks that rax contains a negative number because rax contains a 1 bit in the leftmost position, so it is assumed to be negative. So, be careful with printf!
We will dig somewhat deeper: as soon as the imul instruction is executed, rax contains 0xb14e9f812f364970. In binary, this is 101100010100111010011111100000010010111100110110010010010 with a 1 in the most significant position and hence is negative.
And rdx contains 0x6. That is 000000000000000000000000000000000000000000000000000000110 with a 0 in the most significant position and hence is positive.
The actual product is 0x6b14e9f812f364970 and can be found by combining rdx and rax, in this order: rdx:rax. If you convert this hexadecimal number to decimal, you will find the product you expect: 123456789012345670000. See Figure 9-2.
On the Internet you can find hexadecimal to decimal conversion apps; see https://www.rapidtables.com/convert/number/hex-to-decimal.htmlLet’s continue with integer division, idiv . This is in fact the reverse of multiplication (well, what did you expect?). Divide the dividend in rdx:rax by the divisor in the source operand and store the integer result in rax. The modulo can be found in rdx. It’s important and easy to forget: make sure to set rdx to zero every time before you use idiv or the resulting quotient may be wrong.
64-bit integer multiplication and division have some subtleties for which you can find more details in the Intel manuals. Here we just gave an overview that serves as a general introduction to integer arithmetic. In the Intel manuals, not only will you find more details about the instructions, but you will find a large number of other arithmetic instructions that can be used in specific situations.
Summary
How to do integer arithmetic.
How to do arithmetic shift left and shift right.
Multiplication uses rax and rdx for storing the product.
Division uses rax and rdx for the dividend.
Be careful when using printf when printing values.