RISC-V Linux kernel cross-compilation setup

I’ve recently been building kernels for the VisionFive 2. I started out by using the native toolchain on the device, and got through a few development iterations that way, but it was painfully slow.

I had been reluctant to set up a cross compilation toolchain for kernel development, because I thought it might be fiddly and hard work; I’m quite familiar with building a cross-compilation toolchain for targeting bare-metal RISC-V targets, but I’ve never built one for Linux, and I expected there to be some new challenges I’d have to solve to do that – it turns out there is a relatively simple path to get a working cross-compilation workflow.

This post outlines the setup and use of the workflow I’ve settled on.

Crosstool-ng

My initial attempt was similar to my bare-metal toolchain build process:

  • Install kernel headers somewhere,
  • Build Binutils,
  • Build a Stage 1 GCC,
  • Build the C library (glibc),
  • Build Stage 2 GCC.

The only real difference was the kernel header installation and the use of glibc instead of newlib, so I thought it shouldn’t be too much hassle. However, after a couple of mis-steps that appeared as though they could either be minor, or have knock-on effects later on in the toolchain build or use (it wasn’t clear which) and an ever-growing set of local scripts to perform the build, I capitulated and accepted there must be a better way.

Crosstool-ng can be used to build cross toolchains for many different targets, and it turned out to be apt for my purposes for a RISC-V Linux target.

Building the toolchain

Crosstool-ng includes sample configurations for many different targets (aarch64-rpi4-linux-gnu, mips-unknown-elf, and many others…) and it also includes one for RISC-V Linux: riscv-unknown-linux-gnu. So configuration of the toolchain build was as simple as:

ct-ng riscv64-unknown-linux-gnu

At this point it’s also possible to customise the configuration with:

ct-ng menuconfig

to adjust many aspects of the toolchain (install location, default architecture / ABI, etc.). I found the defaults were OK and didn’t need to change anything – I did configure the ABI as lp64d but I think this might have been the default anyway.

Following configuration, I could build the toolchain with:

ct-ng build

The build takes a few minutes to complete. Afterwards, I put the toolchain on the path with:

export PATH=${TOOLCHAIN_PATH}/bin:${PATH}

Here, TOOLCHAIN_PATH is assumed to be the path to the root of the installation – by default this is ~/x-tools.

At this point, we’re ready to build kernels.

Building kernels

As I went from a native build to a cross-build, I already had a config to use. I pulled this from the device and copied it to the kernel tree:

cd <linux kernel tree>
cp ~/vf2-config .config

Then updated the config for the current setup:

make ARCH=riscv \
     CROSS_COMPILE=riscv64-unknown-linux-gnu- \
     olddefconfig

The kernel, modules and DTBs can then be built with:

make ARCH=riscv \
     CROSS_COMPILE=riscv64-unknown-linux-gnu- \
     Image modules dtbs

I don’t have any modules in the config, so I omit modules, but left it in here for completeness.

Installation / setup on the device

I copied over the image (Image) and DTB (jh7110-starfive-visionfive-2-v1.2a.dtb for my board, yours may differ) onto the VisionFive 2. Following that, we need to:

  • Install the image in /boot,
  • Install the DTB in a location where flash-kernel will find it,
  • Create an initrd, and
  • Update GRUB.

Installing the image is a straightforward copy:

cp ~/Image /boot/vmlinux-6.17.0-rc6-cross

I’m running Ubuntu 25.04 on the VisionFive 2, and in that setup, the DTB needs copying to a location where flash-kernel finds it, as this is run when update-initramfs is run – I’m not sure if this next step is common across all distros, or a peculiarity of my distro choice. One location it will look is /lib/firmware/<kernel-version>/device-tree, so let’s copy it there:

mkdir -p /lib/firmware/6.17.0-rc6-cross/device-tree
cp ~/jh7110-starfive-visionfive-2-v1.2a.dtb \
   /lib/firmware/6.17.0-rc6-cross/device-tree

Afterwards, we can update the initramfs and GRUB:

update-initramfs -c -k 6.17.0-rc6-cross
update-grub

Verifying and testing

Before rebooting, I checked that grub.cfg looked as expected. The relevant parts contained (slightly abridged):

echo    'Loading Linux 6.17.0-rc6-cross ...'
linux   /vmlinuz-6.17.0-rc6-cross \ ...
echo    'Loading initial ramdisk ...'
initrd  /initrd.img-6.17.0-rc6-cross
echo    'Loading device tree blob...'
devicetree      /dtb-6.17.0-rc6-cross

After rebooting, we see the following in dmesg (edited for blog formatting):

Booting Linux on hartid 4
Linux version 6.17.0-rc6-gf93b6ff79cc8-dirty
    (gmarkall@housel)
    (riscv64-unknown-linux-gnu-gcc
        (crosstool-NG 1.28.0.6_620b909) 15.2.0, 
     GNU ld
        (crosstool-NG 1.28.0.6_620b909) 2.45) #3 SMP
    Fri Jan 23 22:41:46 GMT 2026
Machine model: StarFive VisionFive 2 v1.2A

As expected (or hoped!) – the cross-compilation toolchain built earlier was used to compile the kernel.

Iteration

For successive kernel builds, we don’t need to perform all of the above steps. In general, only the kernel needs updating, which can be done after the Image is built and copied to the device:

cp ~/Image /boot/vmlinux-6.17.0-rc6-cross

If the devicetree changed, then it also needs to be copied, but instead of copying to the location referenced by flash-kernel, we can write over the location GRUB loads it from directly:

cp ~/jh7110-starfive-visionfive-2-v1.2a.dtb \
   /boot/dtbs/6.17.0-rc6-cross/starfive

Note: this differs from the location given in grub.cfg above, because the GRUB config references a symlink that points to the actual location written to by the cp command.

Future improvements

The above process is a lot faster and more streamlined than what I was doing originally, but it could still be a little faster. Kexec is a mechanism for a new Linux kernel to be booted from an already-running one. Using it would save having to copy the new kernel into /boot and then reboot.

A nice guide to it (that I intend to try and follow) is given in Rahul Rameshbabu’s blog post on kexec. If / once I get that working, I’ll update this post (or post a follow-up) outlining the process and any other considerations specific to this VisionFive 2 setup.

Leave a comment