Homework 1: Exploring the ARM Instruction Set

This homework is due on Tuseday, February 11, at 11:59:59 PM (Eastern standard time). You must use submit to turn in your homework like so:
submit cs411_jtang hw1 hw1.c calc_grade.S

All of your homework assignments will be run on Linux on the ARMv8-A instruction set. In this homework, you will set up an environment to execute ARMv8-A software and then write some simple assembly code.

You will use the software environment configured by this assignment for all future homework assignments.

This homework is divided into several parts that will help you perform the following objectives:

For this class, you have three choices of development environments:

  1. If your computer runs Windows or macOS, you will need to install Linux in a Virtual Machine. Start with Part 1.
  2. If your computer already runs Linux on a x86-64 architecture, or if you already have an Ubuntu Linux Virtual Machine from CMSC 421, you can reuse that installation. Proceed directly to Part 2.
  3. If your computer runs Linux natively on an ARMv8-A architecture (such as Raspbian on a Raspberry Pi model 3), you can use that as your development environment. Proceed directly to Part 3.

Part 1: Install Linux in a Virtual Machine

Storage Requirements

You will need at least 20 GiB of free space on your computer to store your virtual machine (VM) image, so a 32 GiB or larger drive is highly recommended. If you are storing your VM on a USB flash drive, that drive's filesystem needs to support files of at least > 4 GiB. Many flash drives come pre-formatted as FAT32, which do not support file sizes >= 4 GiB. FAT32 will not work for this reason! You should format your drive as either NTFS (if you will be using Windows with the drive as well) or a Linux filesystem such as ext4.

Linux on VirtualBox

Virtualization allows us to run a virtual machine on an actual physical machine. In this way, we can run a second guest operating system inside the regular host operating system. To do the assignments in this class, we assume you will have access to a relatively modern PC that can run VirtualBox. VirtualBox requires a x86-64 CPU with a decent amount of RAM, at least 4 GiB. In addition, your host CPU should support the x86 virtualization extensions (VT-x for Intel processors, or AMD-V for AMD processors). For more information about hardware and software requirements for VirtualBox, please consult the VirtualBox website. These assignments are written assuming the student has access to a 64-bit VirtualBox instance.

You can download VirtualBox for free from https://www.virtualbox.org/wiki/Downloads to run under your own Windows, macOS, or even Linux host operating system. Follow these instructions to install the 64-bit version of Ubuntu 18.04 LTS.

  1. Install VirtualBox on your computer. This assignment was tested using VirtualBox version 6.0.16.
  2. For optimal VirtualBox performance, you need to enable virtualization on your computer. The exact steps vary based upon computer. See this general guide. (Tablets and convertibles may be incapable.)
  3. Download the Ubuntu 18.04.3 LTS x86 LiveCD. Make sure you download the 64-bit version. You can also make a donation to Ubuntu, if you so desire.
  4. Create a Virtual Machine for your Ubuntu 18.04 installation.
    1. Using VirtualBox, create a new virtual machine. You must give it a name, such as 411VM. Give ample memory (at least 2 GiB) and virtual hard disk space (at least 30 GiB).
    2. Your newly created virtual machine requires additional configuration. Set the number of processors to 2 (or more, if your computer is powerful), and increase video memory to 64 MiB.
    3. Set the boot device to the Ubuntu ISO image you downloaded above. Power on the virtual machine to boot into the Ubuntu LiveCD.
    4. Run the Ubuntu installer. This will take a while, as that the installer will download additional files from the Internet.
    5. After installation, shut down your VM. You should now be able to run Ubuntu without using the LiveCD image.
    6. Remove the LiveCD from the list of disks in the VM.
    7. Reboot the VM, and ensure that it boots into Linux properly.
  5. Use a web browser to ensure that you can connect to the Internet within your VM.
  6. Open a Terminal by clicking on the Application icon in the lower-left corner. In the search box, enter terminal. Right click the icon to add Terminal to your favorites, then run the program.
  7. Update the packages installed on the VM by executing the following command in the Terminal:
    sudo apt-get update && sudo apt-get upgrade
    You will need to enter your password to run the operation as root.
  8. After updating, reboot the VM, to ensure that all updates complete before proceeding. The kernel may be updated by apt, and you must reboot to have the kernel updates applied.
  9. Install VirtualBox Guest Additions, to enable shared clipboard and shared folders.
    1. After the reboot, open a Terminal. Install prerequisites:
      sudo apt-get install build-essential module-assistant
    2. Prepare the virtual machine:
      sude m-a prepare
    3. Choose Insert Guest Additions CD Image from the Devices menu. Run the installer and reboot for a third time.
  10. Take a snapshot of your virtual machine, in case something bad happens in the future.

Many of the commands that you will be running within Linux will require administrator privileges. There are a variety of methods to elevate your user privileges on Linux. You can use any of the following methods to do so:

sudo -s
  (enter your user password when prompted)
  (perform any commands to execute as root)
exit
OR
sudo sh
  (enter your user password when prompted)
  (perform any commands to execute as root)
exit
OR
sudo (command to execute as root)

The instructions in this and all future assignments will explicitly specify when sudo is needed. If it is not needed, do not use the command. Arbitrarily using sudo will not magically fix any issues.

Part 2: Install Cross-Compilation Environment

Your physical computer is (most likely) based upon Intel's x86-64 architecture, but this class is based upon the ARMv8-A architecture. Compilers you have used in other classes are designed to be executed on x86-64 (the native environment) and they generate code that also runs on x86-64 (the target environment). You will need to install a cross-compiler that runs on your x86-64 computer but targets the ARMv8-A architecture. Follow these instructions to install the cross-compiler and ARMv8-A emulator.

These instructions assume the host environment is running Ubuntu 18.04. If you choose to run a different distribution, such as Fedora or Mint, you are responsible for finding the correct commands for your system.

  1. Within a Terminal, install the cross-compiler with this command:
    sudo apt-get install gcc-aarch64-linux-gnu
    Confusingly, Ubuntu uses both the terms aarch64 and arm64 to refer to the ARMv8-A architecture.
  2. Install the ARMv8-A emulator:
    sudo apt-get install qemu-user-static
  3. For ease of development, your software will run in user mode emulation. This is convenient, in that you do not need to install a bootloader, Linux kernel, and root filesystem within the QEMU machine. Instead, you need an ELF interpreter for that target architecture.
    1. Run the following command to enable ARMv8-A multiarch:
      sudo dpkg ‐‐add-architecture arm64
      Note there are two dashes prior to add-architecture, and the final argument is arm64, not aarch64.
    2. Add the arm64 package repository, by creating the file /etc/apt/sources.list.d/sources.list:
      sudo nano /etc/apt/sources.list.d/sources.list
      Use vi or emacs instead of nano, if you prefer. Put these three lines in that file:
      deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic main
      deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic-updates main
      deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic-security main
      Save and quit from the editor.
    3. Fetch from the newly added repositories:
      sudo apt-get update
      You should get warnings that some index files failed to download. It is safe to ignore those warnings.
    4. Now install the ELF cross-loader:
      sudo apt-get install libc6:arm64
If you followed all of the steps correctly, you should have the file /lib/ld-linux-aarch64.so.1. If using VirtualBox, this would be a good time to take another snapshot of your VM.

Part 3: Hello World, ARMv8-A Edition

If running Linux natively on ARMv8-A, install a C compiler before proceeding.

The next task is to write and cross-compile a simple C program, to test your installation.

  1. Create the file hello.c with these contents:
    #include <stdio.h>
    int main(void) {
        printf("Hello, world!\n");
        return 0;
    }
  2. First, compile it natively and then run it, by manually typing in these commands:
    gcc ‐‐std=c99 ‐Wall ‐O0 -g ‐o hello hello.c
    ./hello
    Note the above is dash, dash, "std=c99", and the other flags likewise are preceded by dashes. Copying and pasting the above will not work.
  3. You can tell this hello file is a native binary with this command:
    file hello
    Observe how this is a "x86-64" executable.
  4. Now cross-compile the same program, run it, and then examine its file type:
    aarch64-linux-gnu-gcc ‐‐std=c99 ‐Wall ‐O0 -g ‐o hello hello.c
    ./hello
    file hello
    This time, the binary is reported as "ARM aarch64". Thanks to QEMU user mode emulation, you can still run that program as if it were a native application.

In this class, we will be delving frequently into the world of assembly code. It is very insightful to compare high-level C constructs with the underlying assembly code representation. A good disassembler can easily cost thousands of dollars. Fortunately, our needs are much simpler, so we will use GNU's disassembler:

aarch64-linux-gnu-objdump -S hello
Scroll up to where the main() function is disassembled:
0000000000000724 <main>:
#include <stdio.h>
int main(void) {
 724:   a9bf7bfd        stp     x29, x30, [sp, #-16]!
 728:   910003fd        mov     x29, sp
        printf("Hello, world!\n");
 72c:   90000000        adrp    x0, 0 <_init-0x598>
 730:   911fa000        add     x0, x0, #0x7e8
 734:   97ffffb7        bl      610 <puts@plt>
        return 0;
 738:   52800000        mov     w0, #0x0                        // #0
}
 73c:   a8c17bfd        ldp     x29, x30, [sp], #16
 740:   d65f03c0        ret
  
You will learn what each of these lines do by the end of this semester.

Part 4: ARM Data Types

Now that you are able to compile and execute a ARMv8-A binary, it is time for some programming. Within your Linux environment, create a directory for this homework. Use the following commands to obtain files needed for this homework:

wget https://www.csee.umbc.edu/~jtang/cs411.s20/homework/hw1/hw1.c
wget https://www.csee.umbc.edu/~jtang/cs411.s20/homework/hw1/calc_grade.S
wget https://www.csee.umbc.edu/~jtang/cs411.s20/homework/hw1/Makefile
Examine these three files.

Modify hw1.c to do these things:

  1. Display the number of bytes needed to store variables of these types: bool, char, short, int, long, long long, void *, float, and double. Use the %zu format specifier to display the sizes.
  2. For the variable types char, short, int, long, and long long, determine which ones hold signed and which ones hold unsigned values on the ARMv8-A architecture. Add a comment at the top of your file that lists which variable types are signed, and which ones are unsigned.
  3. For the variable types signed char, signed short, signed int, signed long, and signed long long, modify your C program to calculate the smallest (most negative) and largest (most positive) value a variable of that type could hold. Then display those values.
  4. For the variable types unsigned char, unsigned short, unsigned int, unsigned long, and unsigned long long, modify your C program to calculate the largest (most positive) value a variable of that type could hold. Then display those values.
When displaying the smallest and largest values, do not simply have a line of code like this:
printf("signed char: smallest is -1000, largest is +1000\n");
Instead, devise an algorithm that can calculate those values, then displays the result. (You will get different results if you run this algorithm on x86-64 versus ARMv8-A.)

You cannot use the format specifier %d to display values that do not fit in an int. For example, %zu is the correct format specifier for displaying a value of type size_t. You will need to research the correct specifier for the other variable types, and you will be penalized for using an incorrect specifier. Likewise, you cannot use int to store all numeric values. You will be penalized for using a storage type that results in an incorrect sign/unsigned operation or an overflow.

Part 5: Grade Calculator

As this class involves understanding instruction sets, every assignment will involve writing some kind of assembly code. Your task for this homework is to implement the function calc_grade() within calc_grade.S. This function is to return the letter grade associated a numeric score, as per its Doxygen comments in hw1.c. After implementing your code, run make to compile your program. Here is a sample output when it is run:

$ ./hw1 95
Value of 95 => grade 'A'
$ ./hw1 75
Value of 75 => grade 'C'
$ ./hw1 59
Value of 59 => grade 'F'

As a reminder, register x0 holds the first incoming parameter to a function, and the function's return value is written to x0 prior to the ret instruction.

Other Hints and Notes

Extra Credit

Sorry, there is no extra credit available for this assignment.