用户空间 (Android init/ueventd)内核 (firmware_loader)驱动程序用户空间 (Android init/ueventd)内核 (firmware_loader)驱动程序alt[找到 firmware][未找到]调用 request_firmware()查找 /lib/firmware返回 firmware触发 sysfs fallback发送 ueventinit/ueventd 接收 uevent根据 ueventd.rc 依次查找/vendor/firmware/firmware/image...通过 sysfs fallback 提供 firmware返回 firmware

本文将解析 Android 系统中固件加载的路径和流程,帮助理解驱动程序如何获取所需的固件文件。

固件的加载流程主要由上图所描述,下面我们结合代码来详细说明。

驱动程序调用

驱动程序在需要加载固件时,会调用 request_firmware() 系列函数,这些函数被定义在 Linux 内核的 include/linux/firmware.h 中,以下是高通 CNSS 平台驱动的示例代码:

https://github.com/WANG-Guangxin/wlan-driver/blob/5113495b16420b49004c444715d2daae2066e7dc/platform/cnss2/main.c#L4032

int cnss_request_firmware_direct(struct cnss_plat_data *plat_priv,
				 const struct firmware **fw_entry,
				 const char *filename)
{
	if (IS_ENABLED(CONFIG_CNSS_REQ_FW_DIRECT))
		return request_firmware_direct(fw_entry, filename,
					       &plat_priv->plat_dev->dev);
	else
		return firmware_request_nowarn(fw_entry, filename,
					       &plat_priv->plat_dev->dev);
}

内核处理

对于 CNSS 驱动可能使用到的 request_firmware_direct()firmware_request_nowarn() 函数,他们的实现位于内核的 drivers/base/firmware_loader/main.c 文件中。

request_firmware_direct

request_firmware_direct() 函数直接从文件系统加载固件,不会回退到用户空间的 udev 或者 Android 的 init 进程。

https://github.com/torvalds/linux/blob/260f6f4fda93c8485c8037865c941b42b9cba5d2/drivers/base/firmware_loader/main.c#L1002

/**
 * request_firmware_direct() - load firmware directly without usermode helper
 * @firmware_p: pointer to firmware image
 * @name: name of firmware file
 * @device: device for which firmware is being loaded
 *
 * This function works pretty much like request_firmware(), but this doesn't
 * fall back to usermode helper even if the firmware couldn't be loaded
 * directly from fs.  Hence it's useful for loading optional firmwares, which
 * aren't always present, without extra long timeouts of udev.
 **/
int request_firmware_direct(const struct firmware **firmware_p,
			    const char *name, struct device *device)
{
	int ret;

	__module_get(THIS_MODULE);
	ret = _request_firmware(firmware_p, name, device, NULL, 0, 0,
				FW_OPT_UEVENT | FW_OPT_NO_WARN |
				FW_OPT_NOFALLBACK_SYSFS);
	module_put(THIS_MODULE);
	return ret;
}
EXPORT_SYMBOL_GPL(request_firmware_direct);

firmware_request_nowarn

firmware_request_nowarn() 函数则会尝试从文件系统加载固件,如果失败则会回退到 sysfs fallback 机制,但不会打印警告信息。所谓的 sysfs fallback 机制,就是通过发送 uevent 给用户空间的 init 进程,来查找其他可能的固件路径。

https://github.com/torvalds/linux/blob/260f6f4fda93c8485c8037865c941b42b9cba5d2/drivers/base/firmware_loader/main.c#L977

/**
 * firmware_request_nowarn() - request for an optional fw module
 * @firmware: pointer to firmware image
 * @name: name of firmware file
 * @device: device for which firmware is being loaded
 *
 * This function is similar in behaviour to request_firmware(), except it
 * doesn't produce warning messages when the file is not found. The sysfs
 * fallback mechanism is enabled if direct filesystem lookup fails. However,
 * failures to find the firmware file with it are still suppressed. It is
 * therefore up to the driver to check for the return value of this call and to
 * decide when to inform the users of errors.
 **/
int firmware_request_nowarn(const struct firmware **firmware, const char *name,
			    struct device *device)
{
	int ret;

	/* Need to pin this module until return */
	__module_get(THIS_MODULE);
	ret = _request_firmware(firmware, name, device, NULL, 0, 0,
				FW_OPT_UEVENT | FW_OPT_NO_WARN);
	module_put(THIS_MODULE);
	return ret;
}
EXPORT_SYMBOL_GPL(firmware_request_nowarn);

firmware_fallback_sysfs

无论是 request_firmware_direct() 还是 firmware_request_nowarn(),它们都会调用 _request_firmware() 函数来处理固件加载的具体逻辑。

而在 _request_firmware() 中,我们可以看到这里存在很多 fallback 机制,这里我们重点关注的是 sysfs fallback 机制。

https://github.com/torvalds/linux/blob/260f6f4fda93c8485c8037865c941b42b9cba5d2/drivers/base/firmware_loader/main.c#L906

/* called from request_firmware() and request_firmware_work_func() */
static int
_request_firmware(const struct firmware **firmware_p, const char *name,
		  struct device *device, void *buf, size_t size,
		  size_t offset, u32 opt_flags)
{
	struct firmware *fw = NULL;
	struct cred *kern_cred = NULL;
	const struct cred *old_cred;
	bool nondirect = false;
	int ret;

	if (!firmware_p)
		return -EINVAL;

	if (!name || name[0] == '\0') {
		ret = -EINVAL;
		goto out;
	}


	/*
	 * Reject firmware file names with ".." path components.
	 * There are drivers that construct firmware file names from
	 * device-supplied strings, and we don't want some device to be
	 * able to tell us "I would like to be sent my firmware from
	 * ../../../etc/shadow, please".
	 *
	 * This intentionally only looks at the firmware name, not at
	 * the firmware base directory or at symlink contents.
	 */
	if (name_contains_dotdot(name)) {
		dev_warn(device,
			 "Firmware load for '%s' refused, path contains '..' component\n",
			 name);
		ret = -EINVAL;
		goto out;
	}

	ret = _request_firmware_prepare(&fw, name, device, buf, size,
					offset, opt_flags);
	if (ret <= 0) /* error or already assigned */
		goto out;

	/*
	 * We are about to try to access the firmware file. Because we may have been
	 * called by a driver when serving an unrelated request from userland, we use
	 * the kernel credentials to read the file.
	 */
	kern_cred = prepare_kernel_cred(&init_task);
	if (!kern_cred) {
		ret = -ENOMEM;
		goto out;
	}
	old_cred = override_creds(kern_cred);

	ret = fw_get_filesystem_firmware(device, fw->priv, "", NULL);

	/* Only full reads can support decompression, platform, and sysfs. */
	if (!(opt_flags & FW_OPT_PARTIAL))
		nondirect = true;

#ifdef CONFIG_FW_LOADER_COMPRESS_ZSTD
	if (ret == -ENOENT && nondirect)
		ret = fw_get_filesystem_firmware(device, fw->priv, ".zst",
						 fw_decompress_zstd);
#endif
#ifdef CONFIG_FW_LOADER_COMPRESS_XZ
	if (ret == -ENOENT && nondirect)
		ret = fw_get_filesystem_firmware(device, fw->priv, ".xz",
						 fw_decompress_xz);
#endif
	if (ret == -ENOENT && nondirect)
		ret = firmware_fallback_platform(fw->priv);

	if (ret) {
		if (!(opt_flags & FW_OPT_NO_WARN))
			dev_warn(device,
				 "Direct firmware load for %s failed with error %d\n",
				 name, ret);
		if (nondirect)
			ret = firmware_fallback_sysfs(fw, name, device,
						      opt_flags, ret);
	} else
		ret = assign_fw(fw, device);

	revert_creds(old_cred);
	put_cred(kern_cred);

out:
	if (ret < 0) {
		fw_abort_batch_reqs(fw);
		release_firmware(fw);
		fw = NULL;
	} else {
		fw_log_firmware_info(fw, name, device);
	}

	*firmware_p = fw;
	return ret;
}

firmware_fallback_sysfs() 函数的实现定义在 drivers/base/firmware_loader/fallback.c 中,这个文件只有 200 多行代码,其中实现了向用户空间发送 uevent 的逻辑。

https://github.com/torvalds/linux/blob/260f6f4fda93c8485c8037865c941b42b9cba5d2/drivers/base/firmware_loader/fallback.c#L224

主要的函数调用过程如下:

firmware_fallback_sysfs => fw_load_from_user_helper => fw_load_sysfs_fallback

/**
 * fw_load_sysfs_fallback() - load a firmware via the sysfs fallback mechanism
 * @fw_sysfs: firmware sysfs information for the firmware to load
 * @timeout: timeout to wait for the load
 *
 * In charge of constructing a sysfs fallback interface for firmware loading.
 **/
static int fw_load_sysfs_fallback(struct fw_sysfs *fw_sysfs, long timeout)
{
	int retval = 0;
	struct device *f_dev = &fw_sysfs->dev;
	struct fw_priv *fw_priv = fw_sysfs->fw_priv;

	/* fall back on userspace loading */
	if (!fw_priv->data)
		fw_priv->is_paged_buf = true;

	dev_set_uevent_suppress(f_dev, true);

	retval = device_add(f_dev);
	if (retval) {
		dev_err(f_dev, "%s: device_register failed\n", __func__);
		goto err_put_dev;
	}

	mutex_lock(&fw_lock);
	if (fw_load_abort_all || fw_state_is_aborted(fw_priv)) {
		mutex_unlock(&fw_lock);
		retval = -EINTR;
		goto out;
	}
	list_add(&fw_priv->pending_list, &pending_fw_head);
	mutex_unlock(&fw_lock);

	if (fw_priv->opt_flags & FW_OPT_UEVENT) {
		fw_priv->need_uevent = true;
		dev_set_uevent_suppress(f_dev, false);
		dev_dbg(f_dev, "firmware: requesting %s\n", fw_priv->fw_name);
		kobject_uevent(&fw_sysfs->dev.kobj, KOBJ_ADD); // 发送 uevent 通知
	} else {
		timeout = MAX_JIFFY_OFFSET;
	}

	retval = fw_sysfs_wait_timeout(fw_priv, timeout);
	if (retval < 0 && retval != -ENOENT) {
		mutex_lock(&fw_lock);
		fw_load_abort(fw_sysfs);
		mutex_unlock(&fw_lock);
	}

	if (fw_state_is_aborted(fw_priv)) {
		if (retval == -ERESTARTSYS)
			retval = -EINTR;
	} else if (fw_priv->is_paged_buf && !fw_priv->data)
		retval = -ENOMEM;

out:
	device_del(f_dev);
err_put_dev:
	put_device(f_dev);
	return retval;
}

用户空间处理

firmware 路径的配置文件

在 Android 系统中,固件的加载路径通常由 ueventd.rc 文件配置。这个文件位于 /vendor/etc//system/etc/ 目录下。

在 AOSP 代码库中,ueventd.rc 文件的示例可以在以下路径找到:system/core/rootdir/ueventd.rc

其中 firmware_directories 指令用于指定固件的搜索路径。Android 的 init 进程会根据这些路径来查找固件文件。

注意: 中国大陆屏蔽了 Google 的代码托管服务,无法直接访问 cs.android.com 需要通过 VPN 或其他方式访问。

https://cs.android.com/android/platform/superproject/main/+/main:system/core/rootdir/ueventd.rc

import /vendor/etc/ueventd.rc
import /odm/etc/ueventd.rc

# firmware_directories 用于指定固件的搜索路径
firmware_directories /etc/firmware/ /odm/firmware/ /vendor/firmware/ /firmware/image/  
uevent_socket_rcvbuf_size 16M

subsystem graphics
    devname uevent_devpath
    dirname /dev/graphics

subsystem drm
    devname uevent_devpath
    dirname /dev/dri

subsystem input
    devname uevent_devpath
    dirname /dev/input

subsystem sound
    devname uevent_devpath
    dirname /dev/snd

subsystem dma_heap
   devname uevent_devpath
   dirname /dev/dma_heap

subsystem vfio
    devname uevent_devpath
    dirname /dev/vfio

# ueventd can only set permissions on device nodes and their associated
# sysfs attributes, not on arbitrary paths.
#
# format for /dev rules: devname mode uid gid
# format for /sys rules: nodename attr mode uid gid
# shortcut: "mtd@NN" expands to "/dev/mtd/mtdNN"

/dev/null                 0666   root       root
/dev/zero                 0666   root       root
/dev/full                 0666   root       root
/dev/ptmx                 0666   root       root
/dev/tty                  0666   root       root
/dev/random               0666   root       root
/dev/urandom              0666   root       root
# Aside from kernel threads, only prng_seeder needs access to HW RNG
/dev/hw_random            0400   prng_seeder prng_seeder
/dev/ashmem*              0666   root       root
/dev/binder               0666   root       root
/dev/hwbinder             0666   root       root
/dev/vndbinder            0666   root       root
/dev/vfio/*               0666   root       root

/dev/pmsg0                0222   root       log
/dev/dma_heap/system      0444   system     system
/dev/dma_heap/system-uncached      0444   system     system
/dev/dma_heap/system-secure        0444   system     system

# kms driver for drm based gpu
/dev/dri/*                0666   root       graphics

# these should not be world writable
/dev/uhid                 0660   uhid       uhid
/dev/uinput               0660   uhid       uhid
/dev/rtc0                 0640   system     system
/dev/tty0                 0660   root       system
/dev/graphics/*           0660   root       graphics
/dev/input/*              0660   root       input
/dev/v4l-touch*           0660   root       input
/dev/snd/*                0660   system     audio
/dev/bus/usb/*            0660   root       usb
/dev/mtp_usb              0660   root       mtp
/dev/usb_accessory        0660   root       usb
/dev/tun                  0660   system     vpn
/dev/hidraw*              0660   system     system

# CDMA radio interface MUX
/dev/ppp                  0660   radio      vpn

/dev/kvm                  0666   root       root
/dev/vhost-vsock          0666   root       root

# sysfs properties
/sys/devices/platform/trusty.*      trusty_version        0440  root   log
/sys/devices/virtual/input/input*   enable      0660  root   input
/sys/devices/virtual/input/input*   poll_delay  0660  root   input
/sys/devices/virtual/usb_composite/*   enable      0664  root   system
/sys/devices/system/cpu/cpu*   cpufreq/scaling_max_freq   0664  system system
/sys/devices/system/cpu/cpu*   cpufreq/scaling_min_freq   0664  system system
/sys/devices/virtual/misc/uhid/*/leds/* brightness   0664  system system
/sys/devices/virtual/misc/uhid/*/leds/* multi_intensity   0664  system system

ueventd.rc 文件解析

ueventd.rc 文件中定义了多个固件搜索路径,这些路径会被 Android 的 init 进程加载。

system/core/init/main.cpp 中,会调用 ueventd_main() 函数来处理 uevent 。

using namespace android::init;

int main(int argc, char** argv) {
#if __has_feature(address_sanitizer)
    __asan_set_error_report_callback(AsanReportCallback);
#elif __has_feature(hwaddress_sanitizer)
    __hwasan_set_error_report_callback(AsanReportCallback);
#endif
    // Boost prio which will be restored later
    setpriority(PRIO_PROCESS, 0, -20);
    if (!strcmp(basename(argv[0]), "ueventd")) {
        return ueventd_main(argc, argv); // 处理 ueventd 的主函数
    }
...
    return FirstStageMain(argc, argv);
}

system/core/init/ueventd.cpp 中,ueventd_main() 函数中,会调用静态方法 GetConfiguration() 来读取和解析 ueventd.rc 文件。将解析到的内容保存在 ueventd_configuration 对象中。

然后通过 ueventd_configuration 中的成员变量,也就是 ueventd.rc 文件中定义的内容,来创建各种 uevent 处理器。

其中,就创建了用来处理固件加载的 FirmwareHandler 对象。

https://cs.android.com/android/platform/superproject/main/+/main:system/core/init/ueventd.cpp;l=337;drc=61197364367c9e404c7da6900658f1b16c42d0da;bpv=1;bpt=1

int ueventd_main(int argc, char** argv) {
    /*
     * init sets the umask to 077 for forked processes. We need to
     * create files with exact permissions, without modification by
     * the umask.
     */
    umask(000);

    android::base::InitLogging(argv, &android::base::KernelLogger);

    LOG(INFO) << "ueventd started!";

    SelinuxSetupKernelLogging();
    SelabelInitialize();

	// 维护一个 uevent 处理器的容器
    std::vector<std::unique_ptr<UeventHandler>> uevent_handlers;

	// 获取 ueventd.rc 配置
    auto ueventd_configuration = GetConfiguration();

    UeventListener uevent_listener(ueventd_configuration.uevent_socket_rcvbuf_size);

    // Right after making DeviceHandler, replay all events looking for which
    // block device has the boot partition. This lets us make symlinks
    // for all of the other partitions on the same disk. Note that by the time
    // we get here we know that the boot partition has already shown up (if
    // we're looking for it) so just regenerating events is enough to know
    // we'll see it.
    std::unique_ptr<DeviceHandler> device_handler = std::make_unique<DeviceHandler>(
            std::move(ueventd_configuration.dev_permissions),
            std::move(ueventd_configuration.sysfs_permissions),
            std::move(ueventd_configuration.drivers), std::move(ueventd_configuration.subsystems),
            android::fs_mgr::GetBootDevices(), android::fs_mgr::GetBootPartUuid(), true);
    uevent_listener.RegenerateUevents([&](const Uevent& uevent) -> ListenerAction {
        bool uuid_check_done = device_handler->CheckUeventForBootPartUuid(uevent);
        return uuid_check_done ? ListenerAction::kStop : ListenerAction::kContinue;
    });

    uevent_handlers.emplace_back(std::move(device_handler));

	// 创建固件处理器,这里就传入了 ueventd_configuration 中的固件目录 firmware_directories
    uevent_handlers.emplace_back(std::make_unique<FirmwareHandler>(
            std::move(ueventd_configuration.firmware_directories),
            std::move(ueventd_configuration.external_firmware_handlers)));
...

GetConfiguration() 函数中我们可以看到有哪些 ueventd.rc 会被解析。

static UeventdConfiguration GetConfiguration() {
    if (IsMicrodroid()) {
        return ParseConfig({"/system/etc/ueventd.rc", "/vendor/etc/ueventd.rc"});
    }

    auto hardware = android::base::GetProperty("ro.hardware", "");

    struct LegacyPathInfo {
        std::string legacy_path;
        std::string preferred;
    };
    std::vector<LegacyPathInfo> legacy_paths{
            {"/vendor/ueventd.rc", "/vendor/etc/ueventd.rc"},
            {"/odm/ueventd.rc", "/odm/etc/ueventd.rc"},
            {"/ueventd." + hardware + ".rc", "another ueventd.rc file"}};

    std::vector<std::string> canonical{"/system/etc/ueventd.rc"};

    if (android::base::GetIntProperty("ro.product.first_api_level", 10000) < __ANDROID_API_T__) {
        // TODO: Remove these legacy paths once Android S is no longer supported.
        for (const auto& info : legacy_paths) {
            canonical.push_back(info.legacy_path);
        }
    } else {
        // Warn if newer device is using legacy paths.
        for (const auto& info : legacy_paths) {
            if (access(info.legacy_path.c_str(), F_OK) == 0) {
                LOG(FATAL_WITHOUT_ABORT)
                        << "Legacy ueventd configuration file detected and will not be parsed: "
                        << info.legacy_path << ". Please move your configuration to "
                        << info.preferred << " instead.";
            }
        }
    }

    return ParseConfig(canonical);
}

FirmwareHandler

FirmwareHandler 类是处理固件加载的核心类,它继承自 UeventHandler,并实现了 HandleUevent() 方法来处理接收到的 uevent。

它的构造函数接收固件目录和外部固件处理器的列表,并将这些信息存储在成员变量 firmware_directories_ 中。

https://cs.android.com/android/platform/superproject/main/+/main:system/core/init/firmware_handler.h;l=45?q=FirmwareHandler&ss=android%2Fplatform%2Fsuperproject%2Fmain

class FirmwareHandler : public UeventHandler {
  public:
    FirmwareHandler(std::vector<std::string> firmware_directories,
                    std::vector<ExternalFirmwareHandler> external_firmware_handlers);
    virtual ~FirmwareHandler() = default;

    void HandleUevent(const Uevent& uevent) override;

  private:
    friend void FirmwareTestWithExternalHandler(const std::string& test_name,
                                                bool expect_new_firmware);

    std::string GetFirmwarePath(const Uevent& uevent) const;
    void ProcessFirmwareEvent(const std::string& path, const std::string& firmware) const;
    bool ForEachFirmwareDirectory(std::function<bool(const std::string&)> handler) const;

    std::vector<std::string> firmware_directories_;
    std::vector<ExternalFirmwareHandler> external_firmware_handlers_;
};

HandleUevent() 方法中,FirmwareHandler 类会检查接收到的 uevent 是否与固件相关,并在需要时创建一个子进程来处理固件加载事件。

在子进程中,它会调用 GetFirmwarePath() 方法来获取固件的完整路径,这个 GetFirmwarePath() 方法实际上是根据 uevent 的信息中拿到固件名称。

然后在 ProcessFirmwareEvent() 方法中,会依次遍历 firmware_directories_ 中的路径,尝试加载固件文件。如果找到了固件文件,就会将其写入到 /sys/<path>/data 文件中,并将 /sys/<path>/loading 文件的内容设置为 0,表示固件加载完成。

如果没有找到固件文件,则会打印错误信息,并将 /sys/<path>/loading 文件的内容设置为 -1,表示加载失败。

void FirmwareHandler::HandleUevent(const Uevent& uevent) {
    if (uevent.subsystem != "firmware" || uevent.action != "add") return;

    // Loading the firmware in a child means we can do that in parallel...
    auto pid = fork();
    if (pid == -1) {
        PLOG(ERROR) << "could not fork to process firmware event for " << uevent.firmware;
    }
    if (pid == 0) {
        Timer t;
        auto firmware = GetFirmwarePath(uevent);
        ProcessFirmwareEvent(uevent.path, firmware);
        LOG(INFO) << "loading " << uevent.path << " took " << t;
        _exit(EXIT_SUCCESS);
    }
}

static void LoadFirmware(const std::string& firmware, const std::string& root, int fw_fd,
                         size_t fw_size, int loading_fd, int data_fd) {
    // Start transfer.
    WriteFully(loading_fd, "1", 1);

    // Copy the firmware.
    int rc = sendfile(data_fd, fw_fd, nullptr, fw_size);
    if (rc == -1) {
        PLOG(ERROR) << "firmware: sendfile failed { '" << root << "', '" << firmware << "' }";
    }

    // Tell the firmware whether to abort or commit.
    const char* response = (rc != -1) ? "0" : "-1";
    WriteFully(loading_fd, response, strlen(response));
}

void FirmwareHandler::ProcessFirmwareEvent(const std::string& path,
                                           const std::string& firmware) const {
    std::string root = "/sys" + path;
    std::string loading = root + "/loading";
    std::string data = root + "/data";

    unique_fd loading_fd(open(loading.c_str(), O_WRONLY | O_CLOEXEC));
    if (loading_fd == -1) {
        PLOG(ERROR) << "couldn't open firmware loading fd for " << firmware;
        return;
    }

    unique_fd data_fd(open(data.c_str(), O_WRONLY | O_CLOEXEC));
    if (data_fd == -1) {
        PLOG(ERROR) << "couldn't open firmware data fd for " << firmware;
        return;
    }

    std::vector<std::string> attempted_paths_and_errors;
    auto TryLoadFirmware = [&](const std::string& firmware_directory) {
        std::string file = firmware_directory + firmware;
        unique_fd fw_fd(open(file.c_str(), O_RDONLY | O_CLOEXEC));
        if (fw_fd == -1) {
            attempted_paths_and_errors.emplace_back("firmware: attempted " + file +
                                                    ", open failed: " + strerror(errno));
            return false;
        }
        struct stat sb;
        if (fstat(fw_fd.get(), &sb) == -1) {
            attempted_paths_and_errors.emplace_back("firmware: attempted " + file +
                                                    ", fstat failed: " + strerror(errno));
            return false;
        }
        LOG(INFO) << "found " << file << " for " << path;

		// 这个 LoadFirmware 函数会将固件写入到 data_fd 中
        LoadFirmware(firmware, root, fw_fd.get(), sb.st_size, loading_fd.get(), data_fd.get());
        return true;
    };

    int booting = IsBooting();
try_loading_again:
    attempted_paths_and_errors.clear();
    if (ForEachFirmwareDirectory(TryLoadFirmware)) {
        return;
    }

    if (booting) {
        // If we're not fully booted, we may be missing
        // filesystems needed for firmware, wait and retry.
        std::this_thread::sleep_for(100ms);
        booting = IsBooting();
        goto try_loading_again;
    }

    LOG(ERROR) << "firmware: could not find firmware for " << firmware;
    for (const auto& message : attempted_paths_and_errors) {
        LOG(ERROR) << message;
    }

    // Write "-1" as our response to the kernel's firmware request, since we have nothing for it.
    write(loading_fd.get(), "-1", 2);
}