© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2021
J. BartlettLearn to Program with Assemblyhttps://doi.org/10.1007/978-1-4842-7437-8_5

5. Comparison, Branching, and Looping

Jonathan Bartlett1  
(1)
Tulsa, OK, USA
 

In this chapter, we are going to look at how the computer makes comparisons and decisions.

5.1 The %rip Register and the jmp Instruction

In Chapter 4, we learned about the general-purpose registers. These registers are general purpose because they can be used for most arithmetic instructions. However, there are a few registers which are not usable in this way, but have a specific function that is maintained by the CPU itself. These are the special-purpose registers .

The first register I want to talk about you will probably never need to refer to directly, and that is the instruction pointer, or %rip (it is prefixed with an r because it is a 64-bit register). The instruction pointer simply points to the next memory location that the processor is going to process an instruction from. This lets the CPU know where to pull the next instruction from when the next clock cycle runs. During each instruction, the CPU will increment the instruction pointer to point to the next instruction—the one immediately after the current instruction.

This register can be manipulated through jump instructions. A jump instruction tells the computer to alter the flow of the program by setting the instruction pointer to a value that is different from where the CPU was going to set it to. The most basic form of this instruction is simply jmp, which tells the processor the address of the next instruction you want to execute.

To give you a simple example, the next program will skip over several instructions using the jmp instruction :

jmpexample.s
.globl _start
.section .text
_start:
    movq $7, %rdi
    jmp nextplace
    # These two instructions are skipped
    movq $8, %rbx
    addq %rbx, %rdi
nextplace:
    movq $60, %rax
    syscall

As you can see, just as _start is a label which marked a place in the code where the code begins, in this code, nextplace also marks a location in the code. However, unlike _start, I made up the name nextplace and could call it anything I wanted. _start has a special meaning (it is where the code begins executing), but I can add additional labels anywhere in the code I wish.

Here, nextplace bookmarks the memory location that contains the instruction that follows the label. So, when I issued the instruction jmp nextplace, that tells the CPU to alter its instruction pointer so that the next instruction to execute will be the one at the memory address labeled by nextplace, skipping the two instructions in the middle.

Figure 5-1 gives a conceptual model of what is occurring.
../images/514232_1_En_5_Chapter/514232_1_En_5_Fig1_HTML.png
Figure 5-1

Skipping Instructions Using the Jump Instruction

Here, the instruction flow skips several instructions because of a jmp instruction to the label labelY which is further down in the code.

Not only can you use jmp to skip instructions, you can use it to repeat them as well. Think about it this way, we can jump to a previous section of code which will cause us to repeat it. The following code will cause an infinite loop , which means it will simply repeat itself forever until you stop it (if you aren’t familiar with Linux, simply pressing Ctrl+C will stop the program):

infiniteloop.s
.globl _start
.section .text
_start:
    movq $60, %rax
another_location:
    movq $8, %rdi
    jmp another_location
    # This never gets executed
    syscall

In this program, the label another_location marks a location within the code. Later in the program, we jump back to that location. Then the program will execute again from that location and eventually hit our jump instruction again, in which case it will go back to another_location yet again.

Figure 5-2 gives a conceptual model of what is occurring.
../images/514232_1_En_5_Chapter/514232_1_En_5_Fig2_HTML.png
Figure 5-2

Repeating Instructions Using the Jump Instruction

Here, the instruction flow repeats several instructions because of a jmp instruction to the label labelX which had already occurred previously in the code.

As we have seen, jumps can occur to any location within your code. The next example is a confused nest of jumps. See if you can follow the code and guess what it does before running it:

followthejump.s
.globl _start
.section .text
_start:
    movq $25, %rax
    jmp thelabel
somewhere:
    movq %rax, %rdi
    jmp anotherlabel
label1:
    addq %rbx, %rax
    movq $5, %rbx
    jmp here
labellabel:
    syscall
anotherlabel:
    movq $60, %rax
    jmp labellabel
thelabel:
    movq %rax, %rbx
    jmp there
here:
    divq %rbx
    jmp somewhere
there:
    addq $5, %rbx
    jmp label1
anywhere:
    jmp thelabel

If you got the result wrong, you should try stepping through the program with a debugger as outlined in Appendix C.

5.2 Conditional Jumping and the %eflags Register

The jmp instruction is known as an unconditional jump . That is because it always jumps no matter what. It can be useful, but what ultimately makes a computer powerful is the ability to branch conditionally. A conditional jump is a variant of the jmp instruction that only jumps based on certain conditions.

Unlike higher-level languages, the conditions that are available for a conditional jump instruction are very limited. To understand the conditions that are available for a jump instruction, we have to introduce a new special-purpose register, the %eflags register .1 Rather than thinking about %eflags as holding a single value, you usually think about the different bits of %eflags separately. Each bit holds a true/false status of a previous operation.

Most of the bits of the %eflags register are for operating system usage and aren’t of extreme concern to us. However, there are two flags that come in useful continually:
  • ZF: The zero flag is set to 1 if the result of the last arithmetic operation was zero, or 0 if it was not.

  • CF: The carry flag is set to 1 if the result of the last arithmetic operation resulted in a “carry”—that is, the result was bigger than could be held in the destination register.

There are two more flags that we will deal with in Chapter 8 when we deal with signed numbers.

What happens is that at the end of each arithmetic instruction (instructions like addX, mulX, but not movX), the processor sets the value of these status bits in the %eflags register.

The typical way to make use of these flags is with a conditional jump statement. A conditional jump statement will jump based on the configuration of particular flags. If the condition matches, the jump will occur. Otherwise, the processor will just go to the next instruction as if nothing happened. Common jump instructions include
  • jz: “Jump if Zero” (jump if the zero flag is set to 1).

  • jnz: “Jump if Not Zero” (jump if the zero flag is set to 0).

  • jc: “Jump if Carry” (jump if the carry flag is set to 1).

  • jnc: “Jump if No Carry” (jump if the carry flag is set to 0).

Let us now consider a program which will raise a value to a given power. That is, given the values 2 and 3, it will raise 2 to the 3rd power (i.e., 23, or 2 × 2 × 2, which results in 8). Another interesting feature of exponents is that anything raised to the zeroth power is 1. There’s good reason for that mathematically, but if you’re not a math guy, just trust me on that one.

How would we do this? What we want to do is to take the first value (the base) and multiply it by itself continually. We will use the second value (the exponent) and use it as a counter to keep track of our multiplication. We will run a loop that will continually multiply the current value by the base and decrease our exponent until it is zero, at which time we will leave the loop:

exponent.s
.globl _start
# This will calculate 2^3.
# You can modify %rbx and %rcx to calculate
# another exponential.
.section .text
_start:
    # %rbx will hold the base
    movq $2, %rbx
    # %rcx will hold the current exponent count
    movq $3, %rcx
    # Store the accumulated value in %rax
    movq $1, %rax
mainloop:
    # Adding zero will allow us to use the flags to
    # determine if %rcx has zero to begin with
    addq $0, %rcx
    # If the exponent is zero, we are done
    jz complete
    # Otherwise, multiply the accumulated value by our base
    mulq %rbx
    # Decrease the counter
    decq %rcx
    # Go back to the beginning of the loop and try again
    jmp mainloop
complete:
    # Move the accumulated value to %rdi so we can return it
    movq %rax, %rdi
    # call the "exit" system call
    movq $60, %rax
    syscall

The program starts by loading the initial values into registers. It first loads the base (the number we will be multiplying by itself) into %rbx. Then, the exponent is loaded into %rcx. This will provide a countdown for the number of times we want to multiply %rbx by itself. Finally, a starting value is loaded into %rax. Since anything raised to the zero power is 1, and the base multiplied by 1 is our first number anyway, the program starts by loading a 1 into %rax.

The next instruction is labeled with the mainloop label . This is the point we will return to when repeating the multiplication over and over. The instruction itself may be surprising. The program adds 0 to %rcx. Why would we want to do this?

What we really want to know is whether or not %rcx is zero. However, the movq instruction doesn’t set anything in %eflags. Therefore, by adding 0 to %rcx, this will set the zero flag on %eflags if the result is zero (i.e., if %rcx was zero to begin with).2 Then, if %rcx is already zero (i.e., the program is given a zero exponent), the program jumps to the completion step. Otherwise, it keeps on going.

Next, we multiply by the base that is in %rbx. Remember, the multiply instruction always multiplies with %rax and stores the result there. So, the first time through, %rax will just be the base; the second time through, it will be the base squared; the third time through, it will be the base cubed; etc. Again, this is why %rax is known as the accumulator—many instructions implicitly use this particular register as storing the results of operations.

Next, we decrease %rcx. Here, we are using %rcx as a counter —a number which increases or decreases for every usage. We are using %rcx to keep track of where we are in the multiplying. It starts with the exponent, and every time through the loop we will decrease it by one. When %rcx becomes zero, we know that we have finished all of the multiplications and can stop.

After decrementing %rcx, we jump back to the start of the loop (which is designated in our code with the mainloop label).3 So, if %rcx became zero when it was decremented, then jz instruction will cause it to exit the loop and go to the complete label.

At the complete label, we take %rax, which holds our result, and move it to %rdi in order to return it back to the user. We then do our normal exit system call routine to finish the program.

5.3 Comparisons

In the previous program, when we wanted to see if %rcx was zero, we added zero to it and checked the flags. There’s nothing wrong with that per se, but it is somewhat unintuitive. As a matter of fact, there are a lot of interesting things you can do by just performing arithmetic and checking flags, but doing that makes the code hard to follow.

Thankfully, the instruction set gives us instructions to do explicit comparisons between numbers, as well as several jump instructions which look at the resulting flags and use them to tell the results of the comparison.

The cmpq instruction (and its relatives cmpb, cmpw, and cmpl) compares two numbers to tell which one is larger or if they are both equal. Internally, it performs the comparison by subtracting the numbers (but discarding the result rather than storing it) and then setting the flags accordingly. Then, there are special jump instructions that read the flags and know what that means in terms of which one was larger.

If we issued the command cmpq %rbx, %rax, then the CPU would actually subtract %rbx from %rax, but, rather than storing the result, it would just set the flags and discard the result. The flags will indicate that either %rbx and %rax are the same (the zero flag was set), %rax is greater than %rbx (flags were cleared), or %rax is less than %rbx (the flags set for this are complicated and will be covered in Chapter 8).

Note that the comparison can be of a register with a register, a register with a specific value, or, as we will see later, a register with a value from memory. However, if you are comparing a register with a specific value, the value needs to be placed first in the comparison.

After the cmpq instruction sets the flags, there are corresponding jump instructions that will test one or more of these flags to see whether or not it should jump. Given the command cmpq ARG1, ARG2
  • je will jump if ARG2 equals ARG1.

  • jne will jump if ARG2 does not equal ARG1.

  • ja will jump if ARG2 is above (greater than) ARG1.

  • jae will jump if ARG2 is above (greater than) or equal to ARG1.

  • jb will jump if ARG2 is below (less than) than ARG1.

  • jbe will jump if ARG2 is below (less than) or equal to ARG1.

Note that the order of the arguments is backward than what you might expect. Other conditional jump instructions are available as well, based on a variety of flag configurations, but are not especially helpful for beginners.

5.4 Other Conditional Instructions

The suffixes on the jump instruction—e, ne, a, ae, b, be, and others—are known as condition codes . In addition to conditional jumps, the instruction set has other conditional instructions which utilize these condition codes.

The cmov family of instructions perform conditional moves. It works just like the mov family of instructions, but is based on the same kinds of conditions that the conditional jump instructions use. For instance, cmovgq %rax, %rbx will move the contents of %rax into %rbx if the previous comparison determined a “greater than” condition. Likewise, cmovleq %rax, %rbx will do the same move if it was a “less than or equal” condition.

The loop family of instructions combines several actions into one. What loopq does is the following:
  1. 1.

    Decrement %rcx.

     
  2. 2.

    Jump to the specified label if the result of the decrement is not zero.

     

The %rcx register is known as the “counter” register, because of instructions like loopq which use it to do special count-based actions.

To see the instruction in action, here is the same program again, but this time using a loopq instruction :

exponentloop.s
.globl _start
# This will calculate 2^3.
# You can modify %rbx and %rcx to calculate
# another exponential.
.section .text
_start:
    # %rbx will hold the base
    movq $2, %rbx
    # %rcx will hold the current exponent count
    movq $3, %rcx
    # Store the accumulated value in rax
    movq $1, %rax
    # If the exponent is equal to zero, we are done
    cmpq $0, %rcx
    je complete
mainloop:
    # Multiply the accumulated value by our base
    mulq %rbx
    # Decrement %rcx, go back to loop label if %rcx is
    # not yet zero
    loopq mainloop
complete:
    # Move the accumulated value to %rdi so we can return it
    movq %rax, %rdi
    # call the "exit" system call
    movq $60, %rax
    syscall

Note that the loopq instruction allows the loop to be extremely short. The instruction itself is doing most of the work. It is acting as a conditional control, managing the value of %rcx, and defining the boundaries of the loop. Therefore, the whole loop is just two instructions.

Note that the idea behind this instruction is that it jumps if it is still in the loop and doesn’t jump if you exit the loop. Therefore, this instruction is usually at the tail end of a loop. If your loop requires a condition to enter the loop, that is usually done at the beginning. In our case, we check whether %rcx is zero at the very beginning.

There are also two variants, loopeq and loopneq. These read the %eflags register to give additional conditions to continue looping. loopeq will only continue the loop (i.e., perform a jump) if the previous comparison resulted in equality (i.e., if the zero flag is set). loopneq will only jump if the previous comparison resulted in inequality (i.e., the zero flag is not set).

5.5 A Note About Looping and Branching in Assembly Language

Branching and looping is one area that tends to confuse assembly language programmers who come to assembly language after using other programming languages first. The reason for this is that other programming languages tend to put fences around blocks of code. A for loop functions as a unit, with the start and end of the loop well defined and the control variables spelled out.

In assembly language, however, each instruction is an island unto itself. Many instructions, such as loopq, are built for the purpose of helping you write loops, but there is nothing in assembly language that forces you to use the instructions that way. You could use loopq to decrement %rcx and jump if %rcx is not zero for some non-loop-related reason if you wanted to.

The point is that there is nothing in the language itself that maintains the connection between parts of a loop. You have to maintain that connection by jumping to the right place in your jump, conditional jump, or loop instruction. There are no guardrails in assembly language that make sure you do the right thing.

Because of this, conditionals and loops in assembly language can indeed get messy. In fact, one of the main motivators of higher-level languages was to prevent the messiness of assembly language-style programming, with its proliferation of jump instructions. Historically, many programming languages had a GOTO statement which would perform similar to assembly language jumps. However, it caused code to get so messy that it was essentially taken out of most programming languages, and, for those languages that left it in, programmers were shunned who used it.4

Nonetheless, for assembly language, that’s literally all that we have! The best solution for assembly language programming is to use spacing, labels, and comments in your code to make it clear what your code is doing and why.

Exercises

  1. 1.

    Create your own version of followthejump.s. Walk through the code yourself to be sure you know what it is going to do. Then step through it with the debugger to make sure it does what you expect.

     
  2. 2.

    Create a program that just loops a certain number of times and then exits. Approximately how many times does it have to loop before it takes a full second to run? This number will be very large. Can you estimate how many instructions the CPU executed in that time period?

     
  3. 3.

    Modify the program exponentloop.s several times, each time making it raise a different number to a different power.

     
  4. 4.

    Even though we have already learned about the mulq instruction, write a program that will multiply two numbers by repeatedly adding in a loop.

     
  5. 5.

    Write a program that starts with a value in a register and yields a 1 if that number is even and 0 if that number is odd (hint—think about the divq instruction and remainders).

     
  6. 6.

    Implement the previous program by counting down in a loop rather than using the divq instruction.

     
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.220.1.239