A computer wizard hold C++ in one hand and a BeagleBone Black in the other

How to Cross-Compile C++ for the BeagleBone Black

The modern software developer has a multitude of programming languages at his or her disposal. Some languages, like C#, Java, and Python help speed the development process along by simplifying design. Rich libraries are easily imported. Runtime environments track references and free memory automatically. Concise language syntax abstracts complex programming tasks like threading and synchronization. Yet still, when it comes to performance, C++ remains king of the hill. Sure, there are new challengers to watch, like Rust and Carbon. I have 15 years experience with C++, though, so I plan to stick with the champ when my BeagleBone Black (BBB) projects call for performance.

The way C++ achieves performance is by compiling to native instructions for the processor. Any system calls are made via native APIs provided by the operating system. This is in contrast to interpreted languages and languages that produce an intermediate instruction set. For those such languages, and interpreter or runtime environment must translate the language or intermediate instructions for use by the processor – every time the application is used. A C++ application runs directly on the processor. The binary is machine code compatible to the processor and operating system of the target device. For this reason, it can be challenging to build C++ software. This is especially true when the development host, where source code is compiled, has a different operating system and/or processor architecture than the target device – a procedure known as cross-compilation.

Outline

“If you think it’s simple, then you have misunderstood the problem.”

Bjarne Stroustrup – Inventor of the C++ Programming Language

This guide explains the development environment I have constructed for myself to cross-compile C++ for the BBB.

Objectives

Efficiency is key. My efforts to build a C++ application on the BBB are an after-hours endeavor, and I can’t afford time lost to repetition. My work today, on the development environment, will focus on maximum productivity.

The Development Cycle repeats from Edit to Compile to Debug to Edit Again
Developer focus, and by consequence efficiency, is maximized when the transition time from editing software, to compiling an executable result, to running and troubleshooting that result is minimized.

The development cycle, pictured above, describes the process of creating a compiled application in the most basic form. There is a zone of productivity that must be reached before objectives can be achieved. As a programmer, a range of information is required to be loaded mentally to operate. One has to locate an area of an application or product that needs the new feature or bug fix. The context of the software surrounding that area must be understood. A vision is formed of how to make an improvement or get the next answer in the puzzle. Reference materials are gathered to support the idea. Finally, once the mental load is sufficient, the cycle begins: change some code, get it built, try it out, and keep going until satisfied. This is the zone. The pace of the development cycle while in the zone is paramount, because the zone is fragile!

Machines

I will use 3 computers to get the job done.

Host, Build, and Target Machines

The Host PC is my development workstation. I will author this guide using Microsoft Windows, but the instructions can be adapted for Linux or macOS. The Build machine is where the cross-compiler is installed. I recommend a Virtual Machine (VM) for this purpose. I will use VirtualBox (VBox) to run the Build VM, but other hypervisors will work too. Lastly, I will deploy to and run my binaries on the Target machine, in this case a BBB.

Prerequisites

Equipment

The following equipment is utilized in the instructions.

PartLinkQTYNotes
BeagleBone BlackAmazon1
Network SwitchAmazon1Direct connections from the Host to the Target are problematic when power cycling either device. Use of a switch is strongly recommended.
Gateway RouterAmazon1I anticipate most readers have a router already. For posterity, here’s a consumer-grade recommendation.
Mug WarmerAmazon1A hot beverage is essential for C++ development.

Network

This guide requires configuration of static IP addresses on a network that connects the Host PC, Build VM and Target. Defined below is an example network and the relevant addresses used by this guide. Adapt the values as-necessary for your environment.

DefinitionIPv4Notes
Network192.168.123.0/24/24 indicates netmask 255.255.255.0
Gateway192.168.123.1Typically the LAN address of the router
DNS Server192.168.123.1Not all routers will relay DNS requests – use the DNS from your ISP, if necessary.
Host PC192.168.123.10Static IP address reserved for the Host PC
Build VM192.168.123.20Static IP address reserved for the Build VM
Target192.168.123.30Static IP address reserved for the Target

If the Host PC is unable to reach the Build VM or Target by hostname, I recommend adding entries to C:\Windows\System32\drivers\etc\hosts.

# Copyright (c) 1993-2009 Microsoft Corp.
#
# This is a sample HOSTS file used by Microsoft TCP/IP for Windows.
#
# This file contains the mappings of IP addresses to host names. Each
# entry should be kept on an individual line. The IP address should
# be placed in the first column followed by the corresponding host name.
# The IP address and the host name should be separated by at least one
# space.
#
# Additionally, comments (such as these) may be inserted on individual
# lines or following the machine name denoted by a '#' symbol.
#
# For example:
#
#      102.54.94.97     rhino.acme.com          # source server
#       38.25.63.10     x.acme.com              # x client host

# localhost name resolution is handled within DNS itself.
#	127.0.0.1       localhost
#	::1             localhost

# Takeoff Technical Cross-Compile C++ to BBB Demo
192.168.123.20 build-vm
192.168.123.30 target

Cross-Compilation Information

To successfully build and run a C++ application , one needs to compile for the proper processor architecture of the Target. Additionally, the sources must link to compatible versions of runtime libraries available on the Target. Achieving this compatibility is easiest when the Build VM and Target run the same Linux release.

Processor Architecture

In the case of a BBB, the processor architecture is armhf, or “arm hard float”. My PC runs on an Intel x64 processor, so I will be cross-compiling to armhf.

Linux Release

The BBB is capable of running several different Operating Systems (OS’s). Enterprise shops should consider The Yocto Project (YP) for creating a custom Linux distribution, since the YP caters to the needs of professional embedded products. That said, my needs are different. I intend to learn about hardware interfacing by experimenting with the BBB. The default Debian Linux OS is a wise choice for this purpose. Yes, the YP outputs a toolchain to use for cross compilation, and I won’t get that. However, the overhead of the YP outweighs the effort of setting up a toolchain that will work with the BBB’s Debian OS. Ultimately, I’ll spend less time servicing the Target OS and more time developing prototypes! Besides, many in the BBB community run the Debian OS, so online resources will be more applicable during the learning process.

My Target runs the latest BBB firmware posted to beagleboard.org, which is currently based off Debian 10, codenamed Buster. The exact Debian version can be reviewed by showing the contents of the debian_version file.

you@target:~$ cat /etc/debian_version

Even though the posted firmware is version 10.3, my system reports 10.12. This means I have simply run the typical commands to update the Target via the package manager. If your target is stale, I recommend updating it and rechecking the Debian version before proceeding.

you@target:~$ sudo apt update && sudo apt upgrade

VM Stand-Up

As previously mentioned, when the Build OS and Target OS are the same, odds are better that the compiled application will link and run without issue. It certainly is possible otherwise, but I prefer the consistency when circumstances are easy enough to align the versions. With that, the Debian Buster installation media is available from the Debian Archives. Again, my firmware reported 10.12, so I will use that installer: debian-live-10.12.0-amd64-standard.iso.

The steps I used to create my Build VM are as follows:

  • Create new Virtual Box VM
  • Wizard Page: Name and operating system
  • Wizard Page: Memory size
  • Wizard Page: Hard Disk
  • Wizard Page: Hard disk file type
  • Wizard Page: Storage on physical hard disk
  • Wizard Page: File location and size
  • VirtualBox Manager: Build VM Settings Control
  • VM Settings: System, Processor
  • VM Settings: Storage, SATA Controller
  • Hard Drive Selector: Create
  • Create Drive Wizard: Hard disk file type
  • Create Drive Wizard: Storage on physical hard disk
  • Create Drive Wizard: File location and size
  • Hard Drive Selector: Attach
  • VM Settings: Storage, CD/DVD
  • Optical Drive: Choose a file
  • File Browser: Open an optical file
  • VM Settings
  • File Menu: Virtual Media Manager
  • Virtual Media Manager: Disk Attributes
  • Disk Change Pop-Up Dialog

Running the VM should boot into the Debian DVD image. The steps to install the Debian OS are as follows:

  • Debian Installer: Main Menu
  • Debian Installer: Select a language
  • Debian Installer: Select your location
  • Debian Installer: Configure the keyboard
  • Debian Installer: Configure the network
  • Debian Installer: Configure the network
  • Debian Installer: Set up users and passwords
  • Debian Installer: Set up users and passwords
  • Debian Installer: Set up users and passwords
  • Debian Installer: Set up users and passwords
  • Debian Installer: Configure the clock
  • Debian Installer: Partition disks
  • Debian Installer: Partition disks
  • Debian Installer: Partition disks
  • Debian Installer: Partition disks
  • Debian Installer: Partition disks
  • Debian Installer: Configure the package manager
  • Debian Installer: Configure the package manager
  • Debian Installer: Configure the package manager
  • Debian Installer: Configure the package manager
  • Debian Installer: Install GRUB
  • Debian Installer: Install GRUB
  • Debian Installer: Finish the installation

Virtual Network Configuration

The Build VM now has a clean install of Debian, but the network configuration doesn’t yet match the definition laid out in the prerequisites above. Let’s now take a closer look at the network diagram.

A network diagram of the host, target, and Build VM
One possible solution to network the machines together is to use a bridged virtual adapter from the Build VM to the Host adapter. Then, connecting the Host and Target to a switch results in a physical network of all 3 machines. A router can provide access to the internet for this LAN.

I have wired both the Host and Target to a switch. This switch services a network that I refer to as the Bone Dev LAN. The Build VM will use a bridged interface to join the Bone Dev LAN. A router will serve as the internet gateway and provide DNS relay functionality.

To configure the VM bridge, first locate the Host adapter connected to the Bone Dev LAN.

C:\> ipconfig /all

From the output, identify the description of the Bone Dev adapter.

Ethernet adapter Bone Dev:

   Connection-specific DNS Suffix  . :
   Description . . . . . . . . . . . : Intel(R) Ethernet Server Adapter I350-T2
   Physical Address. . . . . . . . . : A0-36-9F-15-A7-D1
   DHCP Enabled. . . . . . . . . . . : No
   Autoconfiguration Enabled . . . . : Yes
   IPv4 Address. . . . . . . . . . . : 192.168.123.10 (Preferred)
   Subnet Mask . . . . . . . . . . . : 255.255.255.0
   Default Gateway . . . . . . . . . : 192.168.123.1
   DNS Servers . . . . . . . . . . . : 192.168.123.1
   NetBIOS over Tcpip. . . . . . . . : Enabled

Now, modify the VM settings so the virtual network adapter is bridged to this interface.

VM Settings: Network
Simple dropdown menus control network bridging for VirtualBox VMs.

Note: The default virtual adapter configuration is NAT, or Network Address Translation. With NAT, VirtualBox acts as a DHCP server for the guest on a private network – using a subnet of VirtualBox’s choice. Then, at runtime, VirtualBox uses NAT to send out internet traffic using the IP address of the Host on the physical network. NAT is usually quite successful at reaching the internet from a Host that also can reach the internet. The downside, however, is that other machines can’t reach a VM using NAT, since the network used by VirtualBox is private for the guest.

The change to a Bridged adapter will place the VM directly on the LAN. This makes the VM reachable on the LAN, and the VM will have the same access as physical machines on the network. One must recognize that if the LAN does not have a gateway, the VM will lose internet access. Advanced solutions, such as multihoming, are good alternatives for the Build VM when your Bone Dev LAN has no internet. Details of these solutions are beyond the scope of this guide.

Start the VM and login as root. Remove the setup file to avoid nuisance error messages during the boot sequence. Then edit the interfaces file.

root@build-vm:~# rm /etc/network/interfaces.d/setup
root@build-vm:~# nano /etc/network/interfaces

Configure the virtual adapter with the appropriate static ip address for your environment.

# The primary network interface
allow-hotplug enp0s3
iface enp0s3 inet static
  address 192.168.123.20
  netmask 255.255.255.0
  gateway 192.168.123.1

Confirm the /etc/resolv.conf file is configured with the appropriate DNS server for your environment.

root@build-vm:~/# cat /etc/resolv.conf
nameserver 192.168.123.1

Add the IP addresses for the Host PC and the Target to the hosts file.

root@build-vm:~/# nano /etc/hosts
127.0.0.1       localhost
127.0.1.1       build-vm
192.168.123.10  host-pc
192.168.123.30  target

Basic Admin Tasks

Run updates.

root@build-vm:~# apt update && apt upgrade

Setup key environment variables for use later. Tailor the target values to match your BBB and network.

root@build-vm:~# nano /etc/profile.d/build-machine.sh
export BUILD_HOST=`hostname`
export TARGET_BBB=target
export TARGET_USER=you

Add sudo capability for your user. (Replace ‘you’ with your username.)

root@build-vm:~# usermod -aG sudo you

Logout of root and back in as you.

SSH Installation

Like many development environments, SSH will be a cornerstone of the design. Install the SSH server to the Build VM.

you@build-vm:~$ sudo apt install openssh-server

Generate keys.

you@build-vm:~$ ssh-keygen

Defaults are fine.

Generating public/private rsa key pair.
Enter file in which to save the key (/home/you/.ssh/id_rsa):
Created directory '/home/you/.ssh'.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/you/.ssh/id_rsa.
Your public key has been saved in /home/you/.ssh/id_rsa.pub.

I like to rename the public key to avoid confusion when copying it to other machines. (We do this later in the guide.)

you@build-vm:~$ mv id_rsa.pub id_rsa.you.build-vm.pub

Data Disk Configuration

During the setup, a second disk was added to the VM. The purpose of this disk is to store the C++ projects – a design choice to ease maintenance of the VM. The data disk must now be formatted and mounted.

you@build-vm:~$ sudo fdisk /dev/sdb

Make selections to initialize the disk with a GPT partition table and create a single partition consuming the full space available.

Welcome to fdisk (util-linux 2.33.1).
Changes will remain in memory only, until you decide to write them.
Be careful before using the write command.

Device does not contain a recognized partition table.
Created a new DOS disklabel with disk identifier 0xd4a623f8.

Command (m for help): g
Created a new GPT disklabel (GUID: 6AE4D988-F58D-094A-9401-E9543CBAE14F).

Command (m for help): n
Partition number (1-128, default 1):
First sector (2048-209715166, default 2048):
Last sector, +/-sectors or +/-size{K,M,G,T,P} (2048-209715166, default 209715166):

Created a new partition 1 of type 'Linux filesystem' and of size 100 GiB.

Command (m for help): p
Disk /dev/sdb: 100 GiB, 107374182400 bytes, 209715200 sectors
Disk model: VBOX HARDDISK
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: gpt
Disk identifier: 6AE4D988-F58D-094A-9401-E9543CBAE14F

Device     Start       End   Sectors  Size Type
/dev/sdb1   2048 209715166 209713119  100G Linux filesystem

Command (m for help): w
The partition table has been altered.
Calling ioctl() to re-read partition table.
Syncing disks.

Format the new partition as xfs.

you@build-vm:~$ sudo apt install xfsprogs
you@build-vm:~$ sudo mkfs.xfs /dev/sdb1

Setup the build location.

you@build-vm:~$ mkdir ~/dev
you@build-vm:~$ sudo chattr +i ~/dev
you@build-vm:~$ sudo blkid /dev/sdb1
you@build-vm:~$ sudo nano /etc/fstab

Add a permanent mount configuration for the development disk. Substitute the result of the blkid call for the appropriate UUID.

# /etc/fstab: static file system information.
#
# Use 'blkid' to print the universally unique identifier for a
# device; this may be used with UUID= as a more robust way to name devices
# that works even if disks are added and removed. See fstab(5).
#
# / was on /dev/sda1 during installation
# swap was on /dev/sda5 during installation
#
# <file system>                             <mount point>    <type>        <options>           <dump>  <pass>
UUID=4b942b08-3e58-4bd8-bec0-3083ee4a23c2   /                ext4          errors=remount-ro   0       1
UUID=9e88fc74-589e-4ecd-bc88-c3b3d860e5d5   none             swap          sw                  0       0
UUID=37fb13a5-b3ea-463e-9eff-a1849c146078   /home/you/dev    xfs           defaults            0       1
/dev/sr0                                    /media/cdrom0    udf,iso9660   user,noauto         0       0

Mount the disk. Then set the ownership.

you@build-vm:~$ sudo modprobe -v xfs
you@build-vm:~$ sudo mount -a
you@build-vm:~$ sudo chown you:you ~/dev

Install Guest Additions

Attach the VirtualBox guest additions media to the VM.

VM Devices Menu: Insert Guest Additions CD image
you@build-vm:~$ mkdir vbox
you@build-vm:~$ sudo mount /dev/cdrom vbox/
you@build-vm:~$ cd vbox
you@build-vm:~/vbox$ sudo ./VBoxLinuxAdditions.run
you@build-vm:~/vbox$ cd ..
you@build-vm:~$ sudo umount vbox
you@build-vm:~$ rm -rf vbox

During installation, I observed a couple of errors. I believe these are benign, as the VM has no graphical desktop and thus no X.org library to load.

Verifying archive integrity... All good.
Uncompressing VirtualBox 6.1.36 Guest Additions for Linux........
VirtualBox Guest Additions installer
Copying additional installer modules ...
Installing additional modules ...
/opt/VBoxGuestAdditions-6.1.36/bin/VBoxClient: error while loading shared libraries: libXmu.so.6: cannot open shared object file: No such file or directory
/opt/VBoxGuestAdditions-6.1.36/bin/VBoxClient: error while loading shared libraries: libXmu.so.6: cannot open shared object file: No such file or directory
VirtualBox Guest Additions: Starting.
VirtualBox Guest Additions: Building the VirtualBox Guest Additions kernel
modules.  This may take a while.
VirtualBox Guest Additions: To build modules for other installed kernels, run
VirtualBox Guest Additions:   /sbin/rcvboxadd quicksetup <version>
VirtualBox Guest Additions: or
VirtualBox Guest Additions:   /sbin/rcvboxadd quicksetup all
VirtualBox Guest Additions: Building the modules for kernel 4.19.0-21-amd64.
update-initramfs: Generating /boot/initrd.img-4.19.0-21-amd64
VirtualBox Guest Additions: Running kernel modules will not be replaced until
the system is restarted

Now detach the guest additions media. (It’s okay to force the unmount if prompted.)

VM Devices Menu: Remove Guest Additions CD image

Optionally, shut down the VM and take a snapshot at this time. Should future steps in this guide go poorly, restore to this state and try again.

you@build-vm:~$ sudo shutdown -h now
  • VirtualBox Snapshot: Open Manager
  • VirtualBox Snapshot: Take Snapshot
  • VirtualBox Snapshot: New Snapshot Details

I will refrain from recommending snapshots in the remainder of this guide. That said, snapshots are a power tool with virtualization that can save time recovering from errors. One is wise to make use of them often.

VS Code + Remote Extension

There are many editors available for drafting software. I intend not to ignite a holy war by proclaiming one is better than another, but I will say, for me, it isn’t emacs or vi. I will be using Visual Studio Code. It is free. It runs on the big three (Windows, Mac, and Linux). It has productivity features, like autocompletion and symbolic debugging. Most importantly, though, Visual Studio Code has a Remote Development Extension. The C++ and CMake extensions are also prerequisites for this development environment. All three of these extensions are available in the C/C++ Extension Pack released by Microsoft.

To get started with VS Code remote development, one must have a compatible SSH client on the Host machine. Since this guide is using Microsoft Windows as the Host, I will be using the SSH client provided in PowerShell. Additionally, to avoid password typing overload, I will generate a key to exchange with the Build VM.

PS C:\> ssh-keygen

Default selections work.

Generating public/private rsa key pair.
Enter file in which to save the key (C:\Users\you/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in C:\Users\you/.ssh/id_rsa.
Your public key has been saved in C:\Users\you/.ssh/id_rsa.pub.

Now copy the public key to the Build VM.

PS C:\Users\you\.ssh> mv id_rsa.pub id_rsa.you.host.pub
PS C:\Users\you\.ssh> scp .\id_rsa.you.host.pub you@build-vm:~/.ssh/

From the Build VM, this key creates (or is appended to) the authorized_keys file.

you@build-vm:~/.ssh$ cat id_rsa.you.host.pub >> authorized_keys
you@build-vm:~/.ssh$ chmod 600 authorized_keys

You can now SSH to the Build VM without entering a password. Test as-follows.

PS C:\> ssh you@build-vm

Exit then close the PowerShell window. Then open VS Code, and use the remote extensions to establish a session with the Build VM.

  • VS Code: Remote Extension Menu
  • VS Code: Add New SSH Host
  • VS Code: Enter SSH Command
  • VS Code: Select SSH configuration file
  • VS Code: Connect to Host
  • VS Code: Select platform

The VS Code remote instance is now connected to the Build VM. VS Code doesn’t automatically push extensions to the remote instance, however. It is then necessary for us to install the C++ and CMake extensions to the Build VM. Thankfully, the interface is pretty easy, and installing the extension pack takes care of the rest.

VS Code: Extensions Manager
Install the C/C++ Extension pack, which includes CMake, to the Build VM via the button in VS Code’s Extension View.

Toolchain and Target Filesystem

Years ago I cross-compiled C++ to the PowerPC architecture, and it was quite difficult to get a toolchain installed on the build host. Today I had to sit in disbelief after researching for this post in Exploring BeagleBone, by Derek Molloy. The toolchain can be installed with one command! The debugger follows suit with a single command install as well.

you@build-vm:~$ sudo apt-get install crossbuild-essential-armhf
you@build-vm:~$ sudo apt-get install gdb-multiarch

Cross-Build Projects

I was once tasked with prototyping a solution for a company whose dependency management system would not scale any further. I used CMake to demonstrate a new project architecture, and the solution was well-received. Unfortunately, I never got the chance to see that prototype through to implementation, because that company was soon after acquired. (Talk about a change in requirements!) Today, though, I will dust off the CMake experience (read “start over at the tutorial“) and use it to build my C++ software for the BBB.

you@build-vm:~$ sudo apt-get install cmake

One reason to choose CMake is the support for toolchain files, described in Cross Compiling With CMake. Using a toolchain file allows specification of both the compiler and the target environment. For most projects, the cross-compiler needs access to libraries built for the target architecture in order to produce a proper result. Common libraries, in the target architecture, are installed by the crossbuild-essential-armhf package to /usr/arm-linux-gnueabihf. Other libraries are likely to be necessary as well, and for that I will be using a beagleboard.org image.

Stage a Reference Filesystem

Start by downloading the image which you plan to deploy to. Verify the checksum, unzip, then run through fdisk to list out the partitions.

you@build-vm:~$ wget https://debian.beagleboard.org/images/bone-debian-10.3-console-armhf-2020-04-06-1gb.img.xz
you@build-vm:~$ sha256sum bone-debian-10.3-console-armhf-2020-04-06-1gb.img.xz >> checksum.txt
you@build-vm:~$ xz -d bone-debian-10.3-console-armhf-2020-04-06-1gb.img.xz
you@build-vm:~$ sudo fdisk -l bone-debian-10.3-console-armhf-2020-04-06-1gb.img

The fdisk list output shows two important pieces of information: (1) the sector size in bytes and (2) the start sector of the partition that contains the root filesystem.

Disk bone-debian-10.3-console-armhf-2020-04-06-1gb.img: 900 MiB, 943718400 bytes, 1843200 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x411292eb

Device                                             Boot Start     End Sectors  Size Id Type
bone-debian-10.3-console-armhf-2020-04-06-1gb.img1 *     8192 1843199 1835008  896M 83 Linux

The sector size is 512 bytes, and the partition begins at sector 8,192. 512 bytes/sector * 8,192 sectors = 4,194,304 bytes. Divide that by 1,024 bytes/MiB and get an even result of 4 MiB. The offset can thus be specified with the shorthand “4M”.

you@build-vm:~$ mkdir bb-org-img
you@build-vm:~$ sudo mount -o ro,loop,offset=4M bone-debian-10.3-console-armhf-2020-04-06-1gb.img bb-org-img/

Copy the mounted contents to the Build VM.

you@build-vm:~$ mkdir -p ~/dev/targetfs/debian-10-console
you@build-vm:~$ sudo cp -a bb-org-img/* ~/dev/targetfs/debian-10-console/

Clean up.

you@build-vm:~$ sudo umount bb-org-img
you@build-vm:~$ rm -rf bb-org-img/
you@build-vm:~$ rm bone-debian-10.3-console-armhf-2020-04-06-1gb.img checksum.txt

Modify the Reference Filesystem

Debian Linux can do a pretty neat trick — it can emulate an ARM processor and run ARM binaries. This is accomplished with the QEMU (Quick EMUlator) program. By combining QEMU with chroot (change root), we can run commands on the Build VM that update the reference filesystem with ARM packages!

Start by installing the QEMU and schroot packages.

you@build-vm:~$ sudo apt-get install qemu-user-static schroot

Now edit the /etc/schroot/schroot.conf file.

# schroot chroot definitions.
# See schroot.conf(5) for complete documentation of the file format.
#
# Please take note that you should not add untrusted users to
# root-groups, because they will essentially have full root access
# to your system.  They will only have root access inside the chroot,
# but that's enough to cause malicious damage.
#
[debian-10-console]
type=directory
directory=/home/you/dev/targetfs/debian-10-console
description=BeagleBoard.org Debian 10 Console Image for BBB
users=you
root-users=you
personality=linux32

Perform a change root to the reference filesystem as the root user.

you@build-vm:~$ schroot -u root -c debian-10-console

I received an error at this point.

E: 20copyfiles: cp: not writing through dangling symlink '/var/run/schroot/mount/debian-10-console-a8cc8e08-c14c-4746-9435-60431fa1bf9e/etc/resolv.conf'
E: debian-10-console-a8cc8e08-c14c-4746-9435-60431fa1bf9e: Chroot setup failed: stage=setup-start

I resolved this problem by removing the resolv.conf symlink, which points at a non-existent /run/connman/resolv.conf.

you@build-vm:~$ sudo rm ~/dev/targetfs/debian-10-console/etc/resolv.conf
you@build-vm:~$ schroot -u root -c debian-10-console

The prompt should now indicate root within the target filesystem. Perform an update.

(debian-10-console)root@build-vm:~# apt update && apt upgrade
(debian-10-console)root@build-vm:~# exit

I told you it was a neat trick.

CMake Toolchain File

With a target filesystem ready, it’s time to setup a toolchain file.

you@build-vm:~$ mkdir ~/dev/toolchains
you@build-vm:~$ nano ~/dev/toolchains/bbb-debian-10-console.cmake
# This project runs on the Debian Linux BBB console image
set(CMAKE_SYSTEM_NAME Linux)

# These are the cross-compilers installed with Debian's package
set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc CACHE FILEPATH "C compiler path")
set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++ CACHE FILEPATH "C++ compiler path")

# Use the cross compiler package and the target environment as root paths
set(ReferenceFilesystem "$ENV{HOME}/dev/targetfs/debian-10-console")
set(CMAKE_FIND_ROOT_PATH "/usr/arm-linux-gnueabihf")
set(CMAKE_SYSROOT "${ReferenceFilesystem}")

# Adjust the default behavior of the FIND_XXX() commands:
# Search build host for:
#    - Programs
# Search reference filesystem for:
#    - headers, libraries, and packages
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)

# The headers returned from find_package() are not prefixed by the system root 
# of the cross compile. This function will iterate each and prefix the sysroot
# so that the build tools can locate them from the build host.
function(prefix_sysroot_includes found_include_dirs)
    set(replace_include_dirs "")
    foreach(include_dir ${${found_include_dirs}})
        list(APPEND replace_include_dirs "${ReferenceFilesystem}${include_dir}")
    endforeach()
    set(${found_include_dirs} "${replace_include_dirs}" PARENT_SCOPE)
endfunction()

Hello World

I know what you are thinking, “Where the heck is hello world?” Wait no more!

The first task is to setup a CMake project. To begin, get the version of CMake.

you@build-vm:~$ cmake --version
cmake version 3.13.4

CMake suite maintained and supported by Kitware (kitware.com/cmake).

Additionally, determine the C++ standards supported by this compiler.

you@build-vm:~$ arm-linux-gnueabihf-g++ --help -v 2>/dev/null | grep std=c++
  -ansi                       A synonym for -std=c89 (for C) or -std=c++98 (for C++).
  -std=c++03                  Conform to the ISO 1998 C++ standard revised by the 2003 technical corrigendum.  Same
                              as -std=c++98.
  -std=c++0x                  Deprecated in favor of -std=c++11.  Same as -std=c++11.
  -std=c++11                  Conform to the ISO 2011 C++ standard.
  -std=c++14                  Conform to the ISO 2014 C++ standard.
  -std=c++17                  Conform to the ISO 2017 C++ standard.
  -std=c++1y                  Deprecated in favor of -std=c++14.  Same as -std=c++14.
  -std=c++1z                  Deprecated in favor of -std=c++17.  Same as -std=c++17.
  -std=c++2a                  Conform to the ISO 2020(?) C++ draft standard (experimental and incomplete support).
  -std=c++98                  Conform to the ISO 1998 C++ standard revised by the 2003 technical corrigendum.

Now create the main CMake file on disk, and configure it to build hello world. I recommend requiring this version of CMake as a minimum. In the same vein, I recommend compilation using the latest ISO C++ standard.

you@build-vm:~$ mkdir -p ~/dev/hello-x-compile && cd ~/dev/hello-x-compile
you@build-vm:~/dev/hello-x-compile$ nano CMakeLists.txt
# This is the CMake version at the time the application was first built
cmake_minimum_required(VERSION 3.13)

# Create the project for the hello world application
project(hello-x-compile VERSION 1.0)

# The C++ 2017 standard is the newest supported version by gcc at this time
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True)

# Add the executable
add_executable(hello-x-compile main.cpp)

Now for the part we all know and love.

you@build-vm:~/dev/hello-x-compile$ nano main.cpp
#include <stdio.h>
#include <unistd.h>
#include <vector>

using namespace std;

int main(void)
{
    printf("Hello world from Takeoff Technical!\n");

    int count = 0;
    vector<int> testVector;
    for (;;) 
    {
        printf("Count = %d\n", count);
        testVector.push_back(count);
        sleep(1.0); // seconds
        count++;
    }

    return 0;
}

Build with VS Code

Use VS Code’s Open Folder control and input /home/you/dev/hello-x-compile. The extensions will recognize C++ and CMake, and the command palette will prompt for selection of a kit.

VS Code: Command Palette Select a Kit

Note this does not have our toolchain file. Thankfully, though, it’s easy to set that up now. When CMake scans for kits, it saves a user local file, on the Build VM, that caches what it found. Here’s what it looks like now.

you@build-vm:~$ cat ~/.local/share/CMakeTools/cmake-tools-kits.json
[
  {
    "name": "GCC 8.3.0 arm-linux-gnueabihf",
    "compilers": {
      "C": "/usr/bin/arm-linux-gnueabihf-gcc",
      "CXX": "/usr/bin/arm-linux-gnueabihf-g++"
    }
  },
  {
    "name": "GCC 8.3.0 x86_64-linux-gnu",
    "compilers": {
      "C": "/usr/bin/gcc",
      "CXX": "/usr/bin/g++"
    }
  }
]

“To prevent custom kits from being overwritten, give them unique names. CMake Tools will not delete entries from cmake-kits.json, only add and update existing ones.”

CMake Tools Documentation

Add an entry for the toolchain.

[
  {
    "name": "bbb-debian-10-console",
    "toolchainFile": "~/dev/toolchains/bbb-debian-10-console.cmake"
  },
  {
    "name": "GCC 8.3.0 arm-linux-gnueabihf",
    "compilers": {
      "C": "/usr/bin/arm-linux-gnueabihf-gcc",
      "CXX": "/usr/bin/arm-linux-gnueabihf-g++"
    }
  },
  {
    "name": "GCC 8.3.0 x86_64-linux-gnu",
    "compilers": {
      "C": "/usr/bin/gcc",
      "CXX": "/usr/bin/g++"
    }
  }
]

Now the toolchain can be selected as the kit to use by CMake Tools.

VS Code: Command Palette Select a Kit

Use the normal build commands, like the IDE button or the keyboard shortcut F7, to try it out!

Deploy and Debug

In order to run and debug this application, we need to transfer the build result to the target and connect the local and remote debuggers. Some preparation is required on the target.

you@target:~$ sudo apt install gdb
you@target:~$ sudo mkdir -p /opt/builds
you@target:~$ sudo chown you:you /opt/builds

The Build VM also needs to SSH to the target by shared key. If necessary, create the folder for public key storage.

you@target:~$ mkdir ~/.ssh
you@target:~$ chmod 700 ~/.ssh

Then copy the Build VM key to the target.

you@build-vm:~/.ssh$ scp id_rsa.you.build-vm.pub you@target:~/.ssh/

Back on the target, add the Build VM key to the authorized keys.

you@target:~/.ssh$ cat id_rsa.you.build-vm.pub >> authorized_keys
you@target:~/.ssh$ chmod 600 authorized_keys

Now use VS Code’s Run and Debug control to create a custom launch.json file with the following contents.

  • VS Code: Run and Debug
  • VS Code: Command Pallete

Overwrite the contents of launch.json with the following. (Note: credit alan23273850 for the “debuggerPath” setting that fixes the issue of seeing no standard output in the debug console.)

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "BBB Remote GDB",
            "type": "cppdbg",
            "request": "launch",
            "program": "/opt/builds/${env:USER}/${env:BUILD_HOST}/${workspaceRootFolderName}/${command:cmake.getLaunchTargetFilename}",
            "cwd": "/opt/builds/${env:USER}/${env:BUILD_HOST}/${workspaceRootFolderName}/",
            "preLaunchTask": "Deploy to Target",
            "pipeTransport": {
                "pipeCwd": "/usr/bin",
                "pipeProgram": "/usr/bin/ssh",
                "pipeArgs": [ "${env:TARGET_USER}@${env:TARGET_BBB}" ],
                "debuggerPath": "stdbuf -i0 -o0 -e0 /usr/bin/gdb"
            },
            "MIMode": "gdb",
            "miDebuggerPath": "/usr/bin/gdb-multiarch",
            "setupCommands": [
                {
                    "description": "Enable pretty-printing for gdb",
                    "text": "-enable-pretty-printing",
                    "ignoreFailures": false
                },
            ],
            "stopAtEntry": true,
        }
    ]
}

A custom tasks.json is also required to copy build results over to the target.

  • VS Code: Command Palette
  • VS Code: Select a task to configure
  • VS Code: Select a Task Template

Overwrite the contents of tasks.json with the following.

{
    "version": "2.0.0",
    "tasks": [
        {
            "label": "Make Deploy Path",
            "type": "shell",
            "command": "ssh",
            "args": [
                "${env:TARGET_USER}@${env:TARGET_BBB}",
                "mkdir",
                "-p",
                "/opt/builds/${env:USER}/${env:BUILD_HOST}/${workspaceRootFolderName}/"
            ],
        },
        {
            "label": "Deploy to Target",
            "dependsOn": [ "Make Deploy Path", ],
            "type": "shell",
            "command": "scp",
            "args": [
                "${command:cmake.launchTargetPath}",
                "${env:TARGET_USER}@${env:TARGET_BBB}:/opt/builds/${env:USER}/${env:BUILD_HOST}/${workspaceRootFolderName}/",
            ],
        }
    ]
}

Now running the BBB Remote GDB configuration will deploy the binary to the target and run it with symbolic debugging available!

VS Code: View of Active Debug
Symbolic debugging with VS Code and the BeagleBone Black? YES PLEASE!

Adding a Dependency

Most guides stop after hello world. This is unfortunate, because complex programs often have dependencies on 3rd-party libraries. Thankfully, with the development environment constructed thus far, it’s quick and easy to link to extra software. I will demonstrate with a popular Linux library written in C, Simple DirectMedia Layer (SDL).

The first step is to add the library to the reference filesystem. As you may recall, the reference filesystem is where our CMake toolchain is configured to search for headers and libraries. Pulling Debian’s neat trick out once again, use the chroot to install the armhf version of SDL.

you@build-vm:~$ schroot -u root -c debian-10-console
(debian-10-console)root@build-vm:~# apt-get install libsdl2-dev
(debian-10-console)root@build-vm:~# exit

Now add the library to the CMake project.

# This is the CMake version at the time the application was first built
cmake_minimum_required(VERSION 3.13)

# Create the project for the hello world application
project(hello-x-compile VERSION 1.0)

# The C++ 2017 standard is the newest supported version by gcc at this time
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True)

# Add the executable
add_executable(hello-x-compile main.cpp)

# Add the Simple DirectMedia Layer (SDL) Dependency
find_package(SDL2 REQUIRED)
message(STATUS "SDL2 include paths: ${SDL2_INCLUDE_DIRS}")
message(STATUS "SDL2 libraries: ${SDL2_LIBRARIES}")

if(COMMAND prefix_sysroot_includes)
    prefix_sysroot_includes(SDL2_INCLUDE_DIRS)
    message(STATUS "Cross-compile SDL2 include paths: ${SDL2_INCLUDE_DIRS}")
endif()

target_include_directories(hello-x-compile PRIVATE "${SDL2_INCLUDE_DIRS}")
target_link_libraries(hello-x-compile PRIVATE "${SDL2_LIBRARIES}")

The message lines are optional. I have included them to demonstrate how CMake finds the package in the chroot.

[cmake] SDL2 include paths: /usr/include/SDL2
[cmake] SDL2 libraries: -lSDL2
[cmake] Cross-compile SDL2 include paths: /home/you/dev/targetfs/debian-10-console/usr/include/SDL2

Now modify the C++ code to use a few calls from the SDL library.

#include <SDL.h>
#include <stdio.h>

int main(void)
{
    printf("Hello SDL from Takeoff Technical!\n");

    if( SDL_Init(SDL_INIT_VIDEO) < 0 )
    {
        printf("SDL failed to initialize! [%s]\n",
            SDL_GetError()
        );
        return 1;
    }

    SDL_Quit();
    
    return 0;
}

Finally, install the runtime version of the library to the target.

you@target:~$ sudo apt install libsdl2-2.0-0

That’s it! The example now allows building and debugging an application dependent on SDL.

Closing Thoughts

Virtualization and tools like CMake and VS Code enable a developer to stay in the zone when creating C++ software for embedded targets like the BBB. I hope you found this guide helpful!

P.S. The example code is available on GitHub!

Leave a Reply

Microsoft Workplace Discount Program
Scroll to Top
%d bloggers like this: