Recently, I wanted to read the content of a Raspberry Pi’s SD-Card on my MacBook Pro M3. The problem was that macos is not using Linux’s filesystems like ext4, and it’s therefore not possible to mount such SD-Card and read/write its content natively.
What is the best software that can fix my problem ? Linux obviously.
So, my idea was to
- build a virtual machine with a drive being my SD-Card,
- boot a Linux distribution like Debian on it
- and finally mount SD-Card on this Linux VM.
To complicate things I don’t want to use software such as UTM or Orbstack that would make things to easy, but that would also not teach me anything.
Prepare QEMU
First let’s get the tool needed : qemu.
On my machine I used Homebrew to get qemu. As brew.sh says, I ran this command :
brew install qemu
This command installs some binaries and more notably qemu-system-aarch64 and qemu-img. The first binary allows us to start ARM64 virtual machines. The seconds allows us to manipulate disk images.
Build the disk
Before we start the virtual machine let’s actually jump to step 2 and prepare the OS which will be a Debian Linux.
Let’s download a Debian image from here : https://cloud.debian.org/images/cloud/. Why here especially ? Because these are Cloud images which means that we can directly boot them and immediately get a running Debian system without installing it. This repository provides different kind of cloud images. I used the nocloud version for simplicity because I didn’t want to bother with cloud-init on this very little project. Therefore I just ran :
$ curl -OL https://cloud.debian.org/images/cloud/trixie/latest/debian-13-nocloud-arm64.qcow2
Let’s check that the download was fine :
$ qemu-img info images/debian-13-nocloud-arm64.qcow2
image: images/debian-13-nocloud-arm64.qcow2
file format: qcow2
virtual size: 3 GiB (3221225472 bytes)
disk size: 384 MiB
cluster_size: 65536
Format specific information:
compat: 1.1
compression type: zlib
lazy refcounts: false
refcount bits: 16
corrupt: false
extended l2: false
Child node '/file':
filename: images/debian-13-nocloud-arm64.qcow2
protocol type: file
file length: 382 MiB (400984576 bytes)
disk size: 384 MiB
Mmmmh the disk size is only 3GB, which is actually enough for the needs of this project but I always prefer to resize this. Let’s copy this file into a machines directory and resize the disk :
$ mkdir -p machines/dark-maul
$ cp images/debian-13-nocloud-arm64.qcow2 machines/dark-maul/disk.qcow2
$ qemu-img resize machines/dark-maul/disk.qcow2 50G
Let’s check the size now :
$ qemu-img info machines/dark-maul/disk.qcow2
image: machines/dark-maul/disk.qcow2
file format: qcow2
virtual size: 50 GiB (53687091200 bytes)
disk size: 670 MiB
cluster_size: 65536
Format specific information:
compat: 1.1
compression type: zlib
lazy refcounts: false
refcount bits: 16
corrupt: false
extended l2: false
Child node '/file':
filename: machines/dark-maul/disk.qcow2
protocol type: file
file length: 665 MiB (697040896 bytes)
disk size: 670 MiB
Perfect 50GiB ! Notice that it’s virtual, meaning that on my macbook only 670MiB are currently used but the future VM will think that this disk is 50GiB big 🙂
Build and start the VM
The best way to learn qemu is, for me, to read the documentation : https://www.qemu.org/docs/master/
But let’s remember that a VM is just a computer. So theorically to define a VM you would need to define these specs:
- a processor / motherboard
- number of cores
- architecture
- RAM
- IO devices
- disks
- graphic cards
- sound cards
- inputs
- etc…
Board and cpu
The qemu-system-aarch64 command will allow us to define a ARM64 cpu and motherboard. We will do that by adding these flags :
-machine virt,accel=hvf \
-cpu host \
-smp 4
The -machine flag specify the machine (processor/motherboard) we want to emulate. Qemu allows you to emulate many machines. You can find the list by running qemu-system-aarch64 -machine help. We will be emulating the virt machine. About that, the documentation says :
If you don’t care about reproducing the idiosyncrasies of a particular bit of hardware, such as small amount of RAM, no PCI or other hard disk, etc., and just want to run Linux, the best option is to use the
virtboard.
Next to virt you can see accel=hvf. This property enables qemu to use the hypervisor framework of Apple to not emulate but virtualize the VM.
Finally the -smp flag set the number of core to 4.
RAM
To specify the RAM we will add this flag:
-m 4096
This sets 4GB of RAM which is more than enough.
Graphics and network
I don’t want any graphic card on this virtual machine. I only want access to a console. This flag is fine for me :
-nographic
Then I want only one network interface that will give internet access to the VM and I want to be able to SSH to my VM. The following flag will do the trick :
-nic user,hostfwd=tcp::5555-:22
It’s set up an interface in user mode where any TCP connection to localhost port 5555 will be redirected to the guest on localhost port 22 (SSH). This interface mode (user) allows me to run the VM without privileges.
Drive
Now let’s specify the disk the VM will have access to. In order to do that the following flag should do it :
-hda machines/dark-maul/disk.qcow2
By the ways the -hda flag is not recommended read this. More on that later.
BIOS
Something very important to know with ARM64 : there is no default UEFI firmware like in x86 emulation. Therefore you need to have an ARM64 UEFI firmware and you need to specify it. Fortunately for us QEMU provides such firmware. You can find it here :
/opt/homebrew/share/qemu/edk2-aarch64-code.fd
To use this firmware you can add the following flag :
-bios edk2-aarch64-code.fd
First test
Let’s run the complete command :
$ qemu-system-aarch64 \
-machine virt,accel=hvf \
-cpu host \
-smp 4 \
-m 4096 \
-nographic \
-nic user,hostfwd=tcp::5555-:22 \
-hda machines/dark-maul/disk.qcow2 \
-bios edk2-aarch64-code.fd
Everything looks great ! I was asked the Timezone and a first root password. I could then log in as root user !
Let’s try to ssh to the VM now …
ssh root@localhost -p 5555
It doesn’t work. Let’s fix that.
When I run ps -ef | grep ssh, I don’t see any ssh process running.
Let’s install it :
# apt update
# apt install ssh
Now let’s allow root user to log in via SSH (don’t do this in production) :
# sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config
# systemctl restart ssh
I can now login with my root password via SSH.
Share the SD-Card to the guest
Let’s shutdown the VM :
# poweroff
When we specified the main VM’s disk, I mentionned that the -hda flag should be avoided. The documentation recommend to use to flag instead :
-device: defines what guest sees (or frontend)-blockdev: defines what the actual device is (or backend)
For the backend, you must specify a driver for the emulated device. The problem is that it’s not an emulated device but a real one. The driver for our need is host_device. The documentation doesn’t mention it. See this for more information. This driver needs the path on host to the real device, and a node-name to identify this backend.
-blockdev driver=host_device,node-name=sdcard0,filename=/dev/disk4
To find the path to your device on macos you can use the diskutil list command.
For the frontend, you can list all the devices you can emulate with qemu-system-aarch64 -device help.
I’ll use virtio-blk-device driver, I just need to specify the backend with the drive prop.
-device virtio-blk-pci,drive=sdcard0
Let’s start the whole VM again :
$ qemu-system-aarch64 \
-machine virt,accel=hvf \
-cpu host \
-smp 4 \
-m 4096 \
-nographic \
-nic user,hostfwd=tcp::5555-:22 \
-hda machines/dark-maul/disk.qcow2 \
-bios edk2-aarch64-code.fd \
-blockdev driver=host_device,node-name=sdcard0,filename=/dev/disk4 \
-device virtio-blk-pci,drive=sdcard0
You may need to unmount the SD-Card first :
$ diskutil unmountDisk /dev/disk4
On the VM let’s verify that the SD-Card is here.
$ fdisk -l
Bingo ! You can now mount it and read/write its content.