Reading from the Terminal

The C Standard Library offers the scanf function, which may be seen as the analogue for printf just this time for reading instead of printing. Similarly, scanf takes a variable number of arguments, the first one again being a format string. The subsequent arguments are memory addresses, specifying where the values that are read should be stored.

Example: Scanf for an Integer

In C, scanf could be used as follows, to read a 64-bit integer from the terminal:

long num;
scanf("%ld", &num);

Note, how for the second argument, the variable name is prefixed with an ampersand character. This is the C way of specifying that not the value but rather the address of the variable should be used as the argument.

To achieve the same in Assembly, there are multiple approaches possible. Below are 2 common ones, both of which use the stack for storing the value read by scanf. They differ only in the way the stack location is addressed.

  • Base Pointer relative addressing is useful when the value is needed throughout the program/subroutine → as a local variable.

  • Stack Pointer relative addressing is useful when the value should only be stored on the stack until it can be moved to register for further usage, so for values that are only used once/for a short time.

It is up to you which approach you want to use when in your programs. However, it is highly recommended that you try to understand both versions and their differences.

Base Pointer Relative Addressing

.text
input_long_fmt:
    .asciz "%ld"
    
main:
    pushq   %rbp            # save previous base pointer
    movq    %rsp, %rbp      # set base pointer for new stack frame    

    subq    $16, %rsp       # reserve space for 2 local variables
    # ...
    leaq    input_long_fmt(%rip), %rdi
    leaq    -8(%rbp), %rsi  # load address of first local var as 2nd arg
    movb    $0, %al         # 0 vector registers used
    call    scanf
    movq    -8(%rbp), %rax  # move value to rax (e.g., for computations)

The code is very similar to the Example of using printf with Additional Arguments. However, there are a few things to note here:

  • Line 9 reserves 16 bytes of space on the stack. Why? scanf needs the address of a memory location to store the value it reads. The memory that is usually the easiest to access and use is the stack. It might help to revisit the page about Local Variables to understand this step. A single long int (%ld) is 8 bytes long, so why are 16 bytes reserved? As explained in the page on Calling Subroutines the stack should always be 16-byte aligned when calling subroutines. By reserving 16 bytes, instead of 8, this alignment is kept and no adjustments to the stack need to be made before any subroutine calls. The additional 8 bytes can of course be used for other purposes (e.g., when reading another number after the first one).

  • scanf takes a variable number of arguments (similarly as printf) and therefore the number of vector registers needs to be specified in the al register (line 10). For scanf, this will always be 0.

  • scanf stored the value it read, as instructed, on the stack at the location rbp - 8. The last line of the code snippet copies that value, so the value read, from the stack into rax. Here this is done purely for demonstrational purposes, to show how to access the value - you might use the value differently and do not need this step.

Try to follow and understand the given code snippet. How does the stack look after each instruction? Where is the result of the scanf call? It might help to draw the stack and its changes.

Stack Pointer Relative Addressing

.text
input_long_fmt:
    .asciz "%ld"
    
main:
    # ...
    leaq    input_long_fmt(%rip), %rdi
    subq    $8, %rsp        # reserve space for value
    movq    %rsp, %rsi      # load address of reserved space as 2nd arg
    movb    $0, %al         # 0 vector registers used
    subq    $8, %rsp        # fix stack alignment
    call    scanf
    addq    $8, %rsp        # undo the stack alignment fix 
    popq    %rax            # pop result of scanf into rax
  • As opposed to the previous example in which the space on the stack was reserved at the start of the main routine, this time the space on the stack is only reserved right before the call to scanf in line 8. Furthermore, there are only 8 bytes reserved this time with the stack alignment being fixed a bit later - to demonstrate the differences between the part of the stack actually used for the value and the part only reserved as padding for the stack alignment.

  • As the stack pointer always points at (-> holds the address of) the topmost stack element, line 9 simply copies the address of the space reserved with the previous instruction to rsi such that it is the second argument for scanf

  • Lines 11 and 13 are a bit cryptic and don't seem to be doing anything. However, the key point here is stack alignment: As explained before, the stack needs to be 16-byte aligned for subroutine calls. Assuming that the stack is 16-byte aligned at the start of this snippet (line 7), reserving 8 bytes on the stack, as done in line 8, misaligns the stack by 8 bytes. Thereby, another 8 bytes are reserved (line 11) but discarded right after the function returns (line 13). The image below shows the stack frame before and after line 11, highlighting the stack alignment with the dashed green lines.\

  • The last line of the snippet again moves the value from the stack to the rax register. As this is done with a pop instruction, the stack no longer contains the value after this line.

Try to spot the differences from the version shown before and understand why this works the way it does. Again, it might help to draw the stack and its changes.

Last updated