Homework 1: Exploring the ARM Instruction Set

This homework is due on Monday, February 12, 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 calendar.S

All of your homework assignments will be tested within a Linux environment, primarily using 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 several choices for development environments:

  1. If you have an Intel-based Windows or macOS, you will need to install Linux in a Virtual Machine. Start with Part 1.1.
  2. If you are already running Ubuntu 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 1.2.
  3. If your computer runs Linux natively on an ARMv8-A architecture (such as Raspbian on a Raspberry Pi model 4), you can use that as your development environment. Proceed directly to Part 1.3.
  4. If you have an ARM-based Apple computer (so-called Apple Silicon M1 or M2 from November 2020 or newer), you can do these assignments from the command line. See Part 1.4.

Part 1.1: Install Linux in a Virtual Machine

Storage Requirements

You will need at least 25 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 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 to run under your own Windows, macOS, or even Linux host operating system. Follow these instructions to install the 64-bit version of Ubuntu 22.04 LTS; see Ubuntu's site for more details.

VirtualBox 7 has a nasty bug that causes Linux to crash. You have to use VirtualBox 6.1.x for the following to work.

  1. Install VirtualBox 6.1.x on your computer. This assignment was tested using VirtualBox version 6.1.46.
  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 22.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 22.04 installation.
    1. Using VirtualBox, create a new virtual machine. You must give it a name, such as 411VM. Give ample memory (at least 4 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.
    4. Power on the virtual machine. Run the Ubuntu installer. This will take a while, as that the installer will download additional files from the Internet.
    5. After installation, reboot your VM. You should now be able to run Ubuntu without using the LiveCD image.
  5. Power on the VM, to ensure that it boots Linux properly. Use a web browser to ensure that you can connect to the Internet.
  6. Software Updater might run if it detects outdated software. If it does, wait for it to finish execution. After updating, reboot the VM. The kernel may have been updated, and you must reboot to have all updates apply.
  7. 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 that program.
  8. 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. From the Terminal, first navigate to the disc:
      cd /media/gburdell/VBox_GAs_6.1.46
      replacing gburdell with your user name. Then run:
      sudo ./VBoxLinuxAdditions.run
    4. Reboot your virtual machine once more.
    5. Consider adding yourself to the vboxsf group, due to shared folder permissions.
  9. 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 issues.

Part 1.2: Install Cross-Compilation Environment

This class is based upon the ARMv8-A architecture, but your physical computer may be based upon Intel's x86-64 architecture. In other classes, you used compilers to generate code to run on the computer itself (the native environment). That means if your computer is Intel-based, your code will only run 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 x86-64 Ubuntu 22.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. Later assignments require these compilation tools. Run this command:
    sudo apt-get install flex bison libreadline-dev
  3. Install the ARMv8-A emulator. 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, nor root filesystem within the QEMU machine.
    sudo apt-get install qemu-user
  4. Advanced users may want also want debugging tools, as described by this blog post.
If you followed all of the steps correctly, you should have the directory /usr/aarch64-linux-gnu/lib. Test your installation with this command: aarch64-linux-gnu-gcc --version. If using VirtualBox, this would be a good time to take another snapshot of your VM. Go to Part 2, skipping Parts 1.3 and 1.4.

Part 1.3: Install Native Linux on ARMv8-A

If you are running Linux natively on an ARMv8-A architecture, like a Raspberry Pi 4, follow the steps in this part. These instructions were tested on a Raspberry Pi 4 Model B running Raspberry Pi Desktop OS release May 3, 2023.

  1. Download your Linux distribution. Ensure you get the 64-bit version. The default Raspberry Pi OS is only 32-bits. The direct link to 64-bit OS images are at https://downloads.raspberrypi.org/raspios_arm64/images/.
  2. Write your 64-bit Linux distribution to a MicroSD card.
  3. Insert the MicroSD card into your board. Power on your board. Follow the on-screen prompts to finish your installation.
  4. Within a Terminal, install the compiler with this command:
    sudo apt-get install gcc
    If you are running Raspberry Pi Desktop OS, the compiler may already have been installed.
  5. Later assignments require these compilation tools. Run this command:
    sudo apt-get install flex bison libreadline-dev
Test your installation with this command: gcc --version. Go to Part 2, skipping Part 1.4

Part 1.4: Install Apple Silicon Xcode

If you are running macOS on a newer ARM-based Apple computer, follow the steps in this part. You can tell if you have a compatible computer by going to the Apple menu and selecting About This Mac. Apple Silicon Macs will say either "M1" or "M2" as their chip, while older Intel-based Macs will have no indication. These instructions were tested with a M1 2020 MacBook Air running macOS Sonoma 14.3.

  1. From the App Store, install Xcode. Wait for the app to download and install.
  2. From the Utilities folder, run Terminal.
  3. Install Xcode Command Line Tools:
    sudo xcode-select --install
    Click on the Install button to finish installation.
  4. The first time you run Xcode, accept the developer's license.
  5. The Xcode package includes flex, bison, and libreadline. You do not need to run any additional commands to install them.

Test your installation with this command: clang --version.

Part 2: Hello World, ARMv8-A Edition

Now that you have a development environment, 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 if you have an Intel-based computer, the file type is listed as "x86-64" executable. For Linux ARMv8-A, the type is ARM aarch64. For Apple Silicon, the type is arm64.
  4. If running on an Intel-based computer, cross-compile the same program, run it, and then examine its file type:
    aarch64-linux-gnu-gcc -static ‐‐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:

Scroll up to where the main() function is disassembled:
00000000004006d4 
: #include int main(void) { 4006d4: a9bf7bfd stp x29, x30, [sp, #-16]! 4006d8: 910003fd mov x29, sp printf("Hello, world!\n"); 4006dc: f00002a0 adrp x0, 457000 <__dl_iterate_phdr+0xc0> 4006e0: 912f6000 add x0, x0, #0xbd8 4006e4: 94001ab3 bl 4071b0 <_IO_puts> return 0; 4006e8: 52800000 mov w0, #0x0 // #0 } 4006ec: a8c17bfd ldp x29, x30, [sp], #16 4006f0: d65f03c0 ret
Your output will differ slightly if your native environment is already ARMv8-A. You will learn what each of these lines do by the end of this semester.

Part 3: ARM Data Types

Now that you are able to compile and execute an ARMv8-A binary, it is time for some programming. Within your development environment, create a directory for this homework. Use your favorite web browser to download the following files into that directory:

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, float, and double. Use the %zu format specifier to display the sizes.
  2. Next display the sizes of these rarer standard C types: void *, size_t, ssize_t, intptr_t, uintptr_t, and ptrdiff_t.
  3. For the variable types char, short, int, long, long long, size_t, ssize_t, and ptrdiff_t, 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.
  4. 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.
  5. 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 may get different results if you run this algorithm in a native x86-64 environment 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 4: Calendar Calculator

As this class involves understanding instruction sets, most assignments will involve writing some kind of assembly code. Read the comments in hw1.c. Your task for this homework is to implement the function month_calc() within calendar.S. This function is to return how many days are in the given month. After implementing your code, run make to compile your program. Here is a sample output when it is run:

$ ./hw1 1 2024
<Part 3 output intentionally omitted>
Number of days for month 1, year 2024 is: 31
$ ./hw1 2 2024
<Part 3 output intentionally omitted>
Number of days for month 2, year 2024 is: 29
$ ./hw1 13 2024
<Part 3 output intentionally omitted>
Number of days for month 13, year 2024 is: -1

As a reminder, register x0 holds the first incoming parameter to a function, while x1 holds the second parameter. Write the function's return value to x0 prior to the ret instruction. Note that both function's inputs are unsigned values.

Other Hints and Notes

Extra Credit

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