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
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.
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.
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.
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
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.
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:
build-essential includes the C compiler, cc,
the linker ld and assembler as, as well as a
host of other useful tools like make
and patch.
gdb is a debugger for Linux. You've probably used it to
debug programs for which you had the source in the past, but you may not
have known you can use it to probe the behaviour of programs for
which you have no source code!
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!
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 addressip 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 allIf 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=truewhere
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.