主板上的 IRC 客户端
An IRC client in your motherboard

原始链接: https://axleos.com/an-irc-client-in-your-motherboard/

该项目涉及创建一个名为 UEFIRC 的图形 IRC 客户端,旨在在统一可扩展固件接口 (UEFi) 预启动环境中运行。 它用 Rust 编写,利用定制的 GUI 工具包和 TrueType 渲染器,并依赖于 QEMU 中实现的网络后端 vmnet。 用户可以连接到IRC服务器,在UEFi环境中发送和接收消息。 挑战在于为 UEFi 的网络通信开发 Rust 代码,确保传输过程中的内存安全并避免缓冲区分配问题。 UEFIRC 的代码和所需的 UEFi 固件可以在线找到。 该项目展示了 UEFi 在基本硬件初始化之外的高级预启动计算体验方面的潜力。

用户创建了一个不寻常的基于 IrDA(红外数据协会)的 IRC(互联网中继聊天)客户端,名为“UEFIrC”。 它在 UEFI(统一可扩展固件接口)预启动环境中运行,具有 TrueType 字体、光标和图形装饰等高级元素。 最初计划作为从头开始构建 GPS 系统的更轻的替代方案,但由于复杂的图形渲染细节,开发过程超出了预期。 为了保证滚动视图的准确描绘,作者精心建模并通过图表呈现静态视口。 尽管早期计划推出 Twitter 客户端,但现有解决方案仍然存在,促使创建 IRC 客户端以保持原创性。 该用户强调了安全问题,没有使用 HTTP 或其他互联网协议。 他们承认有多种方法可以为非 UEFI 项目建立连接到 VPN 的最小 Linux 系统,并提出了在不直接使用 UEFI 的情况下开发软件的替代方案。 对于那些无法购买支持 PXE(预启动执行环境)的特定主板的用户,可以选择使用较旧的 Intel X915 芯片组以及最少的 Linux 安装和 SSH 连接。 PXE 服务器使客户端能够在 POST(开机自检)期间检索信息,而不是传统的 VPN 凭据交换,从而保护网络基础设施。 通过配置这些设置并确保与 IPMI(智能平台管理接口)控制器的兼容性,用户可以高效地构建更大的集群配置。 此外,开发人员还分享了裸机环境中潜在多媒体应用的灵感,包括合成器、效果处理器以及与吉他和贝司等乐器的外部控制板的音频接口。 这些应用程序需要正确处理声卡。 讨论涉及在滚动视图中处理图形内容时管理过多的内存使用。 为了提高效率,建议实现单独的封装小部件,负责其独特的绘图逻辑。 这种方法具有减少内存需求、缩短初始化时间和提高整体性能等优点。
相关文章

原文

I made a graphical IRC client that runs in UEFI. It’s written in Rust and leverages the GUI toolkit and TrueType renderer that I wrote for axle’s userspace. I was able to develop it thanks to the vmnet network backend that I implemented for QEMU. I’ve published the code here.

You can connect to an IRC server, chat and read messages, all from the comfort of your motherboard’s pre-boot environment.

“Why”? What kind of question is “why”?

A quick refresher on UEFI

The bootloader for any OS is itself loaded with the help of firmware that’s stored on the motherboard’s ROM. Back in the Bad Old Days, this motherboard firmware was a BIOS implementation. This pre-boot environment is often thought of as the first major step to the computer starting up.

BIOS imposed a bunch of annoying limitations, so an industry consortium came up with a new standard to replace it, UEFI.

Just like the BIOS, a UEFI implementation is shipped on each motherboard in ROM. This UEFI firmware provides an environment for the operating system’s bootloader to run in, and provides various APIs that the bootloader can leverage to do its thing.

UEFI is a massive step forwards from BIOS! The bootloader is dropped into a 64-bit environment from the get-go, and UEFI provides tons of helpful APIs for switching VESA display resolutions, allocating memory, and interacting with the EFI filesystem.

UEFI is also somewhat maligned for being over-engineered.

Network boot

One use case that’s kind of fun is that some bootloaders allow the operating system to be loaded over the network, instead of being loaded from a stored installation on a local block device. This can be useful in some corporate environments.

Supporting this use case means that the UEFI firmware ships a network stack, complete with NIC drivers and a TCP implementation, and exposes APIs to interact with this stack directly to any applications running in the pre-boot environment.

Of course, there’s no obligation for the bootloader to actually load an operating system. Behold, social media!

Rust networking in UEFI

The most finicky part of this project by far was implementing a client for UEFI’s TCP protocol in Rust. Making this all work with its scatter-gather buffers was quite tricky.

The nuts and bolts of actually using UEFI’s TCP protocol can be fairly wacky, especially when trying to explain the lifetimes and data interactions to Rust. UEFI’s TCP protocol design enforces the use of global state and re-entrant callbacks, scatter-gather buffers, and an involved set of concepts (events, tokens, handles, protocols, oh my!).

I spent a number of days carefully testing my Rust code to make sure I wasn’t leaking memory, and to squash TCP receive buffer UAFs.

As an example of how the UEFI programming model can be somewhat obtuse, how do you think this UEFI API is meant to be used?

If you gave this to me on a paper napkin, the behavior I’d expect is pretty straightforward:

  1. Specifying NOTIFY_SIGNAL will invoke my callback when an event occurs.
  2. Specifying NOTIFY_WAIT, then calling wait(), will block until an event occurs, invoke my callback, then continue execution.

This is not at all what UEFI does! Here’s how it really works:

  1. If you specify NOTIFY_SIGNAL, UEFI will invoke the callback when the event occurs. Using wait() will raise an error.
  2. If you specify NOTIFY_WAIT, then call wait(), UEFI will invoke your callback whenever it feels like it, multiple times, until the event occurs. Then, the wait() call will unblock.

This is quite confusing, because the callback has completely different semantics depending on which listening mode you use.

According to the UEFI specifiers, when using NOTIFY_SIGNAL, the callback’s world-model should be “The event has occurred, it’s time to do the next thing!”

When using NOTIFY_WAIT, the callback’s world-model should be “The event still hasn’t happened, I should poke or prod something to move things along.”

In other words, UEFI allows you to flip a switch to vacillate between two completely different callback paradigms, one of which is quite nontraditional, and neither of which provide the ‘block until ready’ behavior that both their names sort of imply.

The name on the tin is literally NOTIFY_WAIT, but if you expect it to notify you after the wait() completes, you’ll be in for a bout of confusion.

The actual behavior is spelled out in the docs, but you need to read the WaitForEvent docstring quite closely.

Cursor support

While a mouse isn’t a strict requirement for an IRC client, having one makes the whole app feel more interactive. I used UEFI’s Simple Pointer Protocol to read mouse movement and button presses, and included visual feedback on the cursor’s current position in the GUI.

If you try to use the Simple Pointer Protocol with the OVMF UEFI firmware, you won’t manage to get any mouse events, and you won’t get many helpful errors from the API.

I compiled a custom UEFI firmware build that had all the right drivers and protocols compiled in (paticularly UsbMouseDxe), which allowed me to get on with it. To facilitate others to try out UEFIRC in QEMU, I’ve also uploaded my UEFI firmware to the repo.

Mouse drivers report a change in the mouse’s position. A naive way to code up a mouse cursor might be something like:

As it turns out, this ends up feeling quite sluggish. Operating systems tend to use an approach more like this:

Get a feel for the difference:

People tend to like log2 scaling because you can get where you’re going quicker: drag with confidence, and the mouse will fly across the screen, while still allowing for fine adjustments when honing in on an area more slowly. It’s also just what most people are used to. When presented with a linear movement scaling cursor, people tend to perceive the whole environment as slow and unresponsive.

Modelling IRC messages

Modelling the IRC messages was straightforward and pleasant. IRC uses a textual, line-based format that’s easy to parse, though it’s clearly encumbered by decades of slow expansion, only some of which is standardized.

Using libgui in UEFI

It wasn’t too bad to get my GUI toolkit running in UEFI, as I’ve already done most of the heavy lifting to make axle’s Rust GUI toolkit available in contexts other than axle itself. The first bulk of work here was providing an implementation of AwmWindow that can be used from within UEFI. After that, most of libgui comes for free, including event management, font rendering, layer compositing, view decorations, and tricky components like scroll views.

axle’s Rust-based libgui toolkit came after axle’s C-based libgui toolkit, and the C toolkit still boasts a few features that I haven’t caught up to in the Rust version yet.

For example, the C toolkit displays these nice scroll bars on scroll views.

Since UEFIRC’s primary interaction takes place in a scrolling view filled with text, I couldn’t do without this for any longer. I reimplemented scroll bar functionality in the Rust libgui, with the famous ’tuck-in’ behavior at the top and bottom of the viewport that has made axle the OS of choice for the hip and fashionable the world over.

Text rendering on scroll views

To make UEFIRC usable, I had to make some minor, but notable, changes to how text gets rendered to scrolling views. To see why, let’s first take a look at a simpler case of representing and manipulating pixels.

Representing pixel data is easy if all you have to worry about is a fixed-size rectangle of content. Imagine a buffer with width * height elements, containing RGB data.

When we need to render this rectangle of content somewhere, it’s straightforward to conceptualize copying the buffer corresponding to the desired rectangular region.

Views that allow scrolling their content are much more difficult.

With a fixed-size rectangle, we never need to think twice about how much memory we’ll need to allocate for the pixel buffer. With a scroll view, however, all of a sudden we have an infinitely extensible canvas to think about. Should we impose a ‘maximum size’ on the scroll view and allocate a huge buffer upfront? Should we resize a backing buffer that grows as we draw more content into it?

The approach for scroll views that I went with in axle’s Rust GUI toolkit is based on ’tiles’ of content. Each tile is a square pixel buffer, a few hundred pixels wide, and is the fundamental unit for scroll views.

Each time we draw a bit of graphical content to a scroll view, we first allocate the tiles necessary to display the corresponding visual area.

When rendering a scroll view to another layer, the visible tiles are computed and stitched together into a final image.

This allows the scroll view to expand without bound, and ensures that the scroll view only allocates pixel buffer area that actually contains rendered content, instead of allocating pixel buffer memory for any empty space that the user could scroll to.

This is all to say: it’s a lot more expensive to plot pixels to scrolling views than to fixed-size views, because we have to do more work to maintain the representation.

Drawing graphics primitives to scroll views isn’t too bad, because the scroll view can pre-determine which tiles it’ll need to populate upfront. For example, if a caller asks to draw a circle at a given origin and radius:

The call to allocate_tiles_to_cover_bounding_box() is fairly expensive, but we only have to pay it once for the entire shape. The absolute pathological worst case is putpixel():

Now, let’s look at how the TrueType renderer draws glyphs:

Uh oh! The TrueType renderer is making a ludicrous number of calls to putpixel(), and the underlying scrolling view doesn’t have the opportunity to understand the wider context of how much area the renderer is going to draw to.

To resolve this, I added polygon stacks as one of the ‘fundamental things’ that all view buffer implementations need to know how to draw, alongside primitives like lines, circles and rectangles. This gives the scroll view an opportunity to say “Ah ha, we’re drawing a big polygon! I can allocate all the tiles upfront!”, which is much quicker than the alternative. I don’t love having this as a fundamental primitive, as filling arbitrary polygons conceptually seems quite a bit heftier than rasterizing simpler shapes, but it’s practical and works well.

Improving libgui

Every time I implement a new graphical application against my stack, I come up against little limitations or papercuts that I’ve never run into before. These could be in the GUI toolkit, IPC, driver interface, kernel features, etc. This gives me an opportunity to just-in-time improve the system and APIs to facilitate whatever I’m building at the moment. It’s the fun of OS development! When building out UEFIRC, I made a few notable tweaks and fixes to libgui:

Completely unnecessary

The IRC client itself, as a client, isn’t that usable because this project is an elaborate joke.

However, if you’re ever feeling mad about UEFI’s TCP/IP stack, I know just the tool to complain about it with.

As a final tip of the hat, I joined the UEFI #edk2 development IRC channel from UEFI and bid good tidings.

Here’s a demo showing UEFIRC in action.

联系我们 contact @ memedata.com