Virtualize a Linux environment

This page will help you set up a Linux environment suitable for running the binaries in this course. It's a rough guide than a walkthrough, since each person's setup is different. You are free (and encouraged) to read official documentation too.

We're going to install Ubuntu 22.04 to match the configuration of the CS servers, and follow that up with a demonstration of GDB (which is often the first thing to break if you're using a non-supported environment like WSL 1 or an Arm version of Linux with qemu-user).

Glossary

Host
The operating system on which you are running the virtual machine.
Guest
The operating system (Linux) running inside the virtual machine.

Install a hypervisor and Linux

Windows

On x86_64 Windows we suggest WSL 2. WSL 1 will not work; it does not virtualize a Linux kernel, which is required for many debugging tools. Open a command prompt and type the following:

C:\Users\user> wsl --install --distribution Ubuntu-22.04
Installing: Virtual Machine Platform
Virtual Machine Platform has been installed.
Installing: Windows Subsystem for Linux
Windows Subsystem for Linux has been installed.
Installing: Ubuntu 22.04 LTS
Ubuntu 22.04 LTS has been installed.
The requested operation is successful. Changes will not be effective until the system is rebooted.

Once you have rebooted, you can open a shell in your Ubuntu VM by using the start menu entry or the Windows Terminal option. You may be prompted with some setup questions. When it is running, you can check operating system version in command prompt. Confirm that you are using Ubuntu-22.04 and WSL version 2.

C:\Users\user> wsl --list --verbose
  NAME            STATE           VERSION
* Ubuntu-22.04    Running         2

Continue on to the setting up Linux section.

MacOS

There are two flavours of Apple computers in wide use as of writing this.

We suggest downloading UTM, a free and open source hypervisor that works for either type of mac. Crucially, UTM also supports emulating x86_64 on Arm, which is required to run x86_64 Linux operating systems on Apple silicon computers. Fortunately the computers are fast enough that this emulation is rarely noticeable as long as there's no graphical desktop.

  1. Once you've installed UTM, download the Ubuntu 22.04 image. If you're not sure, choose the "server" image (the "desktop" image comes with a graphical environment, which is unusably slow to emulate, and unnecessary for us in either case).
  2. In UTM, select "create a new virtual machine", then "emulate" or "virtualize" depending on your computer, then "other," then find the Ubuntu image you downloaded. The rest of the options you're given should be fine to keep default, though you may want to change the allocated RAM, CPU, or maximum disk space, and probably the name of the VM.
  3. Then, click the play button on the newly-created VM and proceed to install Ubuntu. (After accepting all the defaults, this can take 10 minutes or more.) The only things that need configuration are your username (e.g. "user"), password (does not need to be secure), and hostname (e.g. "vm").

Some rebooting will happen. You've succeeded once you see the login prompt:

Ubuntu 22.04.3 LTS vm tty1

vm login:

Before you can wreak havoc on your virtual computer, note that the default UTM window isn't particularly usable. It doesn't let you copy text, scroll back, or resize it. The easiest way to fix this is to access your VM via SSH using Terminal.app. Continue to setting up Linux for details.

Setting up Linux

In your Linux shell, verify that you have Ubuntu 22.04 for x86_64.

user@vm:~$ uname -m
x86_64

You can also check the Ubuntu release:

user@vm:~$ cat /etc/os-release
PRETTY_NAME="Ubuntu 22.04.3 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
VERSION="22.04.3 LTS (Jammy Jellyfish)"
VERSION_CODENAME=jammy
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=jammy

File transfer and SSH

It's worth practicing how to transfer files to and from the virtual machine. How you do this is up to you. In WSL, your host filesystem is found under /mnt/c/. Under UTM, you can set up a shared directory or SSH in to your VM and transfer files with scp or rsync (they are good tools to know how to use regardless).

To use SSH, you'll need to know your VM's IP address. This can be found by looking at the output of ip addr inside the VM:

user@vm:~$ ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: enp0s1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 7a:5f:5b:b0:f6:7f brd ff:ff:ff:ff:ff:ff
    inet 192.168.64.14/24 metric 100 brd 192.168.64.255 scope global dynamic enp0s1
       valid_lft 85271sec preferred_lft 85271sec
    inet6 fd13:6faa:efa4:717e:785f:5bff:feb0:f67f/64 scope global dynamic mngtmpaddr noprefixroute
       valid_lft 2591926sec preferred_lft 604726sec
    inet6 fe80::785f:5bff:feb0:f67f/64 scope link
       valid_lft forever preferred_lft forever

Your virtualized network card will begin with en, and the IPv4 address given next to inet should be accessible from the host operating system. You should try SSHing into the VM by typing ssh user@192.168.64.14 in a shell on the host. You can avoid password prompts in the future by setting up an SSH key and copying it to the VM with ssh-copy-id user@192.168.64.14.

If you have a file called foo.txt on the host, you can now copy it to the guest with scp foo.txt user@192.168.64.14:foo.txt. Reverse the arguments to copy a file from the guest to the host.

As an exercise, try downloading this program for Linux, transferring it into the VM, and running it. You'll likely need to chmod +x it.

Basic tools

It's important that you are able to compile, run, and debug programs on Linux. For that, you'll need a few tools, all of which are available through Ubuntu's package manager. Here's what we're going to install:

user@vm:~$ sudo apt-get update
[…]
Fetched 2,523 kB in 4s (563 kB/s)
Reading package lists... Done
user@vm:~$ sudo apt-get install -y build-essential gdb
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following additional packages will be installed:
  g++ g++-11 libbabeltrace1 libboost-regex1.74.0 libc6-dbg
  libdebuginfod-common libdebuginfod1 libdw1 libipt2
  libsource-highlight-common libsource-highlight4v5 libstdc++-11-dev
[…]

We'll try out some of the tools on the same example binary as before. You are encouraged to follow along. First, observe that the file is an ELF binary for x86_64 Linux:

user@vm:~$ file example
example: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=286d7b1a32c01e644c784eb385fa4f2564538445, for GNU/Linux 3.2.0, stripped
    

The objdump command lets us peek inside the compiled program. Notice the code to load an address into the register rdi before the call to puts.

user@vm:~$ objdump -d example
[…]
Disassembly of section .text:

0000000000001060 <main>:
    1060:	f3 0f 1e fa          	endbr64
    1064:	48 83 ec 08          	sub    $0x8,%rsp
    1068:	48 8d 3d 95 0f 00 00 	lea    0xf95(%rip),%rdi        # 2004 <_IO_stdin_used+0x4>
    106f:	e8 dc ff ff ff       	call   1050 <puts@plt>
    1074:	31 c0                	xor    %eax,%eax
    1076:	48 83 c4 08          	add    $0x8,%rsp
    107a:	c3                   	ret
    107b:	0f 1f 44 00 00       	nopl   0x0(%rax,%rax,1)
[…]
    

We can watch the code in action in GDB.

user@vm:~$ gdb example
Reading symbols from example...
(No debugging symbols found in example)
(gdb)

GDB complains about there not being any debug symbols in the program, but we're not going to let that stop us.

(gdb) b main
Breakpoint 1 at 0x1060
(gdb) r
Starting program: /home/user/example
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 1, 0x0000555555555060 in main ()
(gdb)

Unlike using GDB with debug symbols, we don't get the source listing that would usually appear. We can instead ask to see a disassembly of the current function:

(gdb) disassemble
Dump of assembler code for function main:
=> 0x0000555555555060 <+0>:	endbr64
   0x0000555555555064 <+4>:	sub    $0x8,%rsp
   0x0000555555555068 <+8>:	lea    0xf95(%rip),%rdi        # 0x555555556004
   0x000055555555506f <+15>:	call   0x555555555050 <puts@plt>
   0x0000555555555074 <+20>:	xor    %eax,%eax
   0x0000555555555076 <+22>:	add    $0x8,%rsp
   0x000055555555507a <+26>:	ret
End of assembler dump.

We see it's the same as before, but with an => indicating the location of the program counter. We step into or over instructions just like we would statements using the si and ni commands (respectively).

(gdb) ni
0x0000555555555064 in main ()
(gdb) ni
0x0000555555555068 in main ()
(gdb) ni
0x000055555555506f in main ()

The program counter is now just about to execute the call to puts. We can print what was loaded into rdi by printing the special variable $rdi (the same goes for any register).

(gdb) p $rdi
$1 = 93824992239620

Well that wasn't too informative, but it makes sense. puts takes a const char * as its first argument, which would be loaded as an address into rdi before jumping to the code. If we want to see what will be printed, we need to use the x command, which prints the contents of memory at a location. We can specify that we want to print a null-terminate strings with the /s format specifier.

(gdb) x/s $rdi
0x555555556004:	"it worked!"

Let's be evil and overwrite the bytes pointed to by rdi.

(gdb) set ((char *)$rdi)[0] = 'a'
(gdb) set ((char *)$rdi)[1] = 0
(gdb) ni
a
0x0000555555555074 in main ()

If you've made it this far, your Linux setup is ready and working, and you can debug programs!

Troubleshooting

If you run into issues, you can try to search (e.g. on Google) for any errors/messages that come up; many common Linux problems or WSL/UTM problems are encountered by other users as well who may have solutions. You can also post on Piazza - students, TAs and instructors may have encountered similar issues and have solutions ready.

Below are some of the common issues that have been encountered along with possible solutions. This is not meant to be an exhaustive list, so you should use one of the other resources (searching the Web, Piazza posting) if your issue is not listed here, or if the solution does not work.

ip addr shows a network interface, but it's DOWN and has no IP address
If you run ip addr and get output like the following:
user@vm:~$ ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: enp0s1: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 7a:5f:5b:b0:f6:7f brd ff:ff:ff:ff:ff:ff
with no IP address visible, this can signal that the network interface was not configured properly during the installation process. Check the output of the following command:
sudo netplan get all
If the network interface was configured correctly, you would see something like this (where enp0s1 may be replaced with a different name):
network:
  version: 2
  ethernets:
    enp0s1:
      dhcp4: true
If you see no output instead, then the network wasn't configured properly. In that case, run the command
sudo netplan set ethernets.enp0s1.dhcp4=true
where enp0s1 should be replaced by the name of your network interface - the identifier after the 2: in the ip addr output. Then, reboot the VM.
macOS: some programs cannot connect to the VM's IP
If you're on macOS, and you find you can't connect to the VM on certain programs, it might be due to the macOS security settings. Under System Preferences (or System Settings), Privacy & Security, Local Network, ensure you've added every program that needs access to the VM (e.g. Terminal, VS Code, etc.). If you do not have the Local Network option, then you may be facing a different issue.