编辑
2025-03-30
记录知识
0

ukui-window-switch是麒麟系统上的后台任务管理工具,其实际上是通过kwin来加载的特效动态库。最近我们的git仓库都通过git上ci,由ci来执行构建,但是发现ukui-window-switch软件包在本地构建正常,在ci上构建后功能异常,本文根据此问题介绍排查思路和解决办法

一、ukui-window-switch是运行机制

为了了解此问题,我们需要清楚ukui-window-switch的运行机制,对于ukui-window-switch包,其内容如下:

root@kylin:~/1# dpkg -L ukui-window-switch /. /usr /usr/bin /usr/bin/ukui-window-switch /usr/lib /usr/lib/aarch64-linux-gnu /usr/lib/aarch64-linux-gnu/qt5 /usr/lib/aarch64-linux-gnu/qt5/plugins /usr/lib/aarch64-linux-gnu/qt5/plugins/ukui-kwin /usr/lib/aarch64-linux-gnu/qt5/plugins/ukui-kwin/effects /usr/lib/aarch64-linux-gnu/qt5/plugins/ukui-kwin/effects/plugins /usr/lib/aarch64-linux-gnu/qt5/plugins/ukui-kwin/effects/plugins/libwindowsview.so /usr/share /usr/share/doc /usr/share/doc/ukui-window-switch /usr/share/doc/ukui-window-switch/changelog.Debian.gz /usr/share/doc/ukui-window-switch/copyright /usr/share/kservices5 /usr/share/kservices5/ukui-kwin /usr/share/kservices5/ukui-kwin/kwin4_window_switcher_thumbnail_grid.desktop /usr/share/ukui-kwin /usr/share/ukui-kwin/tabbox /usr/share/ukui-kwin/tabbox/thumbnail_grid /usr/share/ukui-kwin/tabbox/thumbnail_grid/contents /usr/share/ukui-kwin/tabbox/thumbnail_grid/contents/ui /usr/share/ukui-kwin/tabbox/thumbnail_grid/contents/ui/main.qml /usr/share/ukkcoreaddons/ui-kwin/tabbox/thumbnail_grid/metadata.desktop

可以发现,其关键点是libwindowsview.so的动态库文件。而加载此动态库的程序是kwin,所以我们留意kwin的代码如下:

QList<KPluginMetaData> ScriptedEffectLoader::findAllEffects() const { #if defined(QT_NO_DEBUG) QString packageRoot = QStringLiteral("ukui-kwin/effects"); #else QString packageRoot = kwinApp()->applicationDirPath() + QLatin1String("/../../effects"); if (access(packageRoot.toStdString().c_str(), F_OK) == -1) packageRoot = QStringLiteral("ukui-kwin/effects"); qDebug() << "Load effects from:" << packageRoot; #endif return KPackage::PackageLoader::self()->listPackages(s_serviceType, packageRoot); }

而对于effect的load,则在loadEffect函数

bool PluginEffectLoader::loadEffect(const QString &name) { const auto info = findEffect(name); if (!info.isValid()) { return false; } return loadEffect(info, LoadEffectFlag::Load); }

此时我们关注factory函数,如下:

EffectPluginFactory *PluginEffectLoader::factory(const KPluginMetaData &info) const { if (!info.isValid()) { return nullptr; } QString fileName = info.fileName(); if (0 == info.pluginId().compare("UKUI-KWin-Windows-View")) { QString tmpFile = qEnvironmentVariableIsSet("UKUI-KWin-Windows-View_LIBRARY") ? qgetenv("UKUI-KWin-Windows-View_LIBRARY") : info.fileName(); if (QFile::exists(tmpFile)) fileName = tmpFile; } KPluginLoader loader(fileName); if (loader.pluginVersion() != KWIN_EFFECT_API_VERSION) { qDebug() << info.pluginId() << " has not matching plugin version, expected " << KWIN_EFFECT_API_VERSION << "got " << loader.pluginVersion(); return nullptr; } KPluginFactory *factory = loader.factory(); if (!factory) { qDebug() << "Did not get KPluginFactory for " << info.pluginId(); return nullptr; } return dynamic_cast< EffectPluginFactory* >(factory); }

根据上面的代码,我们可以知道其so的加载过程,接下来我们查看报错信息

二、报错信息

我们知道ukui_kwin的错误日志在ukui_kwin_0.log中,所以找到错误如下:

"UKUI-KWin-Windows-View" has not matching plugin version, expected 229 got 4294967295

这里我们可以发现,我们期望的so的version是229,但是得到的是4294967295。我们计算4294967295的值是0xffffffff

而针对代码,我们留意宏如下:

KWIN_EFFECT_API_VERSION

然后留意函数

loader.pluginVersion()

三、排查问题

对于kwin,我们知道其KWIN_EFFECT_API_VERSION是229,而对于ukui-window-switch,我们需要找到so的version填入代码,如下

windowsview/multitaskviewmanagerpluginfactory.cpp
class MultitaskViewManagerPluginFactory : public KWin::EffectPluginFactory { Q_OBJECT Q_INTERFACES(KPluginFactory) Q_PLUGIN_METADATA(IID KPluginFactory_iid FILE "windowsview.json") public: MultitaskViewManagerPluginFactory() {} ~MultitaskViewManagerPluginFactory() override {} KWin::Effect* createEffect() const override { return new MultitaskView::MultitaskViewManager(); } }; K_EXPORT_PLUGIN_VERSION(KWIN_EFFECT_API_VERSION)

这里我们看到K_EXPORT_PLUGIN_VERSION会导出KWIN_EFFECT_API_VERSION

回到kwin。我们也同样查看定义 K_EXPORT_PLUGIN_VERSION

#define KWIN_EFFECT_API_MAKE_VERSION( major, minor ) (( major ) << 8 | ( minor )) #define KWIN_EFFECT_API_VERSION_MAJOR 0 #define KWIN_EFFECT_API_VERSION_MINOR 229 #define KWIN_EFFECT_API_VERSION KWIN_EFFECT_API_MAKE_VERSION( \ KWIN_EFFECT_API_VERSION_MAJOR, KWIN_EFFECT_API_VERSION_MINOR ) K_EXPORT_PLUGIN_VERSION(quint32(KWIN_EFFECT_API_VERSION))

这里我们注意K_EXPORT_PLUGIN_VERSION宏的实现,如下

/** * \relates KPluginLoader * Use this macro if you want to give your plugin a version number. * You can later access the version number with KPluginLoader::pluginVersion() */ #define K_EXPORT_PLUGIN_VERSION(version) \ Q_EXTERN_C Q_DECL_EXPORT const quint32 kde_plugin_version = version;

这里可以发现,K_EXPORT_PLUGIN_VERSION实际上是const关键字的kde_plugin_version。

现在的疑问是ukui-kwin和ukui-window-switch的定义一模一样,而且都是229,为什么会出现问题呢。

3.1 查看静态变量的真实值

我们虽然代码都确定了是229,但是我们报错信息很明显是一个229.一个是0xffffffff。我们现在发现了kde_plugin_version是一个const值,所以可以通过二进制工具直接查看值大小。

对于本地编译的so,我们查找kde_plugin_version的真实值

# readelf -s libwindowsview.so | grep kde_plugin_version 491: 0000000000039e6c 4 OBJECT GLOBAL DEFAULT 13 kde_plugin_version

此时得到0x0000000000039e6c的偏移地址,然后objdump这个动态库如下:

# objdump -s libwindowsview.so | grep "^ 39e" 39e0 00000000 00000000 0b9d0000 12000000 ................ 39e08 4d756c74 69746173 6b566965 774d616e MultitaskViewMan 39e18 61676572 506c7567 696e4661 63746f72 agerPluginFactor 39e28 79000000 00000000 08000000 00000000 y............... 39e38 00000000 00000000 00000000 00000000 ................ 39e48 00000000 00000000 00000000 00000000 ................ 39e58 00000000 00000000 00000000 00000000 ................ 39e68 00000000 e5000000 001b00ec 37fd0075 ............7..u 39e78 006b0075 0069002d 00770069 006e0064 .k.u.i.-.w.i.n.d 39e88 006f0077 002d0073 00770069 00740063 .o.w.-.s.w.i.t.c 39e98 0068005f 007a0068 005f0043 004e002e .h._.z.h._.C.N.. 39ea8 0071006d 001b07ec 323d0075 006b0075 .q.m....2=.u.k.u 39eb8 0069002d 00770069 006e0064 006f0077 .i.-.w.i.n.d.o.w 39ec8 002d0073 00770069 00740063 0068005f .-.s.w.i.t.c.h._ 39ed8 0062006f 005f0043 004e002e 0071006d .b.o._.C.N...q.m 39ee8 00060703 7dc30069 006d0061 00670065 ....}..i.m.a.g.e 39ef8 00730003 0000783c 0071006d 006c000f .s....x<.q.m.l..

我们发现0x0000000000039e6c的值是0x000000e5,也就是229。

对于ci编译的so,我们查找kde_plugin_version的值:

# readelf -s libwindowsview.so | grep kde_plugin_version 660: 000000000003a8e4 4 OBJECT GLOBAL DEFAULT 13 kde_plugin_version

可以看到,其偏移值是0x000000000003a8e4,然后我们将其objdump出来

# objdump -s libwindowsview.so | grep "^ 3a8" 3a80 00000000 00000000 18280000 12000000 .........(...... 3a800 56003100 30000000 6f72672e 756b7569 V.1.0...org.ukui 3a810 2e4b5769 6e000000 2f4d756c 74697461 .KWin.../Multita 3a820 736b5669 65770000 6f72672e 6b64652e skView..org.kde. 3a830 4b506c75 67696e46 6163746f 72790000 KPluginFactory.. 3a840 33334d75 6c746974 61736b56 6965774d 33MultitaskViewM 3a850 616e6167 6572506c 7567696e 46616374 anagerPluginFact 3a860 6f727900 00000000 ffffffff 21000000 ory.........!... 3a870 00000000 00000000 18000000 00000000 ................ 3a880 4d756c74 69746173 6b566965 774d616e MultitaskViewMan 3a890 61676572 506c7567 696e4661 63746f72 agerPluginFactor 3a8a0 79000000 00000000 08000000 00000000 y............... 3a8b0 00000000 00000000 00000000 00000000 ................ 3a8c0 00000000 00000000 00000000 00000000 ................ 3a8d0 00000000 00000000 00000000 00000000 ................ 3a8e0 00000000 e5000000 001b00ec 37fd0075 ............7..u 3a8f0 006b0075 0069002d 00770069 006e0064 .k.u.i.-.w.i.n.d

我们找到0x000000000003a8e4 的值是0x000000e5其十进制也是229

所以我们知道,这个问题和编译构建没关系,二进制生成出来都是229,那问题出在加载时的匹配逻辑上,那个0xffffffff应该是被强制设置的。

3.2. 确定运行时加载so时version的值

为了确认运行时的函数,我们需要留意如下:

KPluginLoader loader(fileName); if (loader.pluginVersion() != KWIN_EFFECT_API_VERSION) {

我们关注类KPluginLoader

其实现在kcoreaddons的src/lib/plugin/kpluginloader.cpp,如下

quint32 KPluginLoader::pluginVersion() { Q_D(const KPluginLoader); if (!load()) { return qint32(-1); } return d->pluginVersion; }

可以发现,确实被人设置为-1了,正好-1就是0xffffffff那就证明了load()函数失败了。

我们留意这个load函数

bool KPluginLoader::load() { Q_D(KPluginLoader); if (!d->loader->load()) { return false; } if (d->pluginVersionResolved) { return true; } Q_ASSERT(!fileName().isEmpty()); QLibrary lib(fileName()); Q_ASSERT(lib.isLoaded()); // already loaded by QPluginLoader::load() // TODO: this messes up KPluginLoader::errorString(): it will change from unknown error to could not resolve kde_plugin_version quint32 *version = reinterpret_cast<quint32 *>(lib.resolve("kde_plugin_version")); if (version) { d->pluginVersion = *version; } else { d->pluginVersion = ~0U; } d->pluginVersionResolved = true; return true; }

我们可以知道一定是d->loader→load()返回失败,我们注意这个loader的类型,如下:

class KPluginLoaderPrivate { Q_DECLARE_PUBLIC(KPluginLoader) protected: KPluginLoaderPrivate(const QString &libname) : name(libname), loader(nullptr), pluginVersion(~0U), pluginVersionResolved(false) {} ~KPluginLoaderPrivate() {} KPluginLoader *q_ptr; const QString name; QString errorString; QPluginLoader *loader; quint32 pluginVersion; bool pluginVersionResolved; };

可以发现loader是QPluginLoader *loader;

至此我们知道了是qt的plugin加载时存在问题,导致229被强制赋值为-1.

3.3 调试QT_DEBUG_PLUGINS

为了能够监听到ukui-kwin加载时load动态库的过程,也就是查看QPluginLoader 的加载过程,我们有一个宏配置可以查看,如下测试验证:

kill -9 $(pidof /usr/bin/ukui-kwin_x11) QT_DEBUG_PLUGINS=1 /usr/bin/ukui-kwin_x11

此时我们的日志在/home/kylin/.log/ukui_kwin_0.log

我们得到如下信息:

250211 09:56:08.820 Debug[19478]: 无法加载库/usr/lib/aarch64-linux-gnu/qt5/plugins/ukui-kwin/effects/plugins/libwindowsview.so:(/usr/lib/aarch64-linux-gnu/qt5/plugins/ukui-kwin/effects/plugins/libwindowsview.so: undefined symbol: glXGetFBConfigAttrib) 250211 09:56:08.821 Warning[19478]: QLibraryPrivate::loadPlugin failed on "/usr/lib/aarch64-linux-gnu/qt5/plugins/ukui-kwin/effects/plugins/libwindowsview.so" : "无法加载库/usr/lib/aarch64-linux-gnu/qt5/plugins/ukui-kwin/effects/plugins/libwindowsview.so:(/usr/lib/aarch64-linux-gnu/qt5/plugins/ukui-kwin/effects/plugins/libwindowsview.so: undefined symbol: glXGetFBConfigAttrib)" 250211 09:56:08.821 Debug[19478]: "UKUI-KWin-Windows-View" has not matching plugin version, expected 229 got 4294967295 250211 09:56:08.821 Debug[19478]: Couldn't get an EffectPluginFactory for: "UKUI-KWin-Windows-View"

我们抓住关键信息:undefined symbol: glXGetFBConfigAttrib

我们可以知道这个是glx相关的函数,但是我们的系统使用的是glesv2,故我们可以屏蔽。

四、解决

我们找到glXGetFBConfigAttrib的调动地址如下:

grep -nr glXGetFBConfigAttrib 匹配到二进制文件 obj-aarch64-linux-gnu/windowsview/CMakeFiles/windowsview.dir/glxtexturehandler.cpp.o windowsview/glxtexturehandler.cpp:319: glXGetFBConfigAttrib(dpy, configs[i], GLX_RED_SIZE, &red); windowsview/glxtexturehandler.cpp:320: glXGetFBConfigAttrib(dpy, configs[i], GLX_GREEN_SIZE, &green); windowsview/glxtexturehandler.cpp:321: glXGetFBConfigAttrib(dpy, configs[i], GLX_BLUE_SIZE, &blue); windowsview/glxtexturehandler.cpp:327: glXGetFBConfigAttrib(dpy, configs[i], GLX_VISUAL_ID, (int *) &visual); windowsview/glxtexturehandler.cpp:333: glXGetFBConfigAttrib(dpy, configs[i], GLX_BIND_TO_TEXTURE_RGBA_EXT, &bind_rgba); windowsview/glxtexturehandler.cpp:334: glXGetFBConfigAttrib(dpy, configs[i], GLX_BIND_TO_TEXTURE_RGB_EXT, &bind_rgb); windowsview/glxtexturehandler.cpp:340: glXGetFBConfigAttrib(dpy, configs[i], GLX_BIND_TO_TEXTURE_TARGETS_EXT, &texture_targets); windowsview/glxtexturehandler.cpp:346: glXGetFBConfigAttrib(dpy, configs[i], GLX_DEPTH_SIZE, &depth); windowsview/glxtexturehandler.cpp:347: glXGetFBConfigAttrib(dpy, configs[i], GLX_STENCIL_SIZE, &stencil);

这里可以发现windowsview/glxtexturehandler.cpp会调用libgl.so的api。引入此符号的原因是glxtexturehandler.cpp.o被成功链接到libwindowsview.so中了,但实际上我们并不需要。我们查看构建日志如下:

https://dev.kylinos.cn/+librarian/14396573/buildlog_kylin-desktop-v101-arm64.ukui-window-switch_3.1.0.1-0k0.1tablet8rk1.egf0.1build1_BUILDING.txt.gz

我们留意ld这一步,如下:

image.png

可以发现其可重定向文件.o被引用到so中。

根据cmakelists.txt的描述,我们可以根据HAVE_GLX来判断构建时是否加入

set(SRCS abstracthandler.cpp concretetexturehandler.cpp glxtexturehandler.cpp egltexturehandler.cpp windowthumbnail.cpp desktopbackground.cpp multitaskviewmanagerpluginfactory.cpp ) # glxtexturehandler.cpp is discarded when HAVE_GLX is not set if (${HAVE_GLX}) list(APPEND SRCS glxtexturehandler.cpp) endif() # translation find_package(QT NAMES Qt6 Qt5 COMPONENTS LinguistTools REQUIRED) find_package(Qt${QT_VERSION_MAJOR} COMPONENTS LinguistTools REQUIRED)

修改之后,我们再次构建如下:

image.png

可以发现ld链接libwindowsview.so的时候,不会加入glxtexturehandler.cpp.o了。此问题解决。

编辑
2025-03-30
记录知识
0

系统调用eventfd介绍

eventfd是一个利用匿名文件设计的系统调用,用作高效的进程间通信,本文介绍一下eventfd的内核实现和用户测试。方便后续编程时可以考虑使用eventfd

内核实现

eventfd是通过syscall实现,如下

SYSCALL_DEFINE2(eventfd2, unsigned int, count, int, flags) { return do_eventfd(count, flags); } SYSCALL_DEFINE1(eventfd, unsigned int, count) { return do_eventfd(count, 0); }

其实现如下:

static int do_eventfd(unsigned int count, int flags) { struct eventfd_ctx *ctx; struct file *file; int fd; /* Check the EFD_* constants for consistency. */ BUILD_BUG_ON(EFD_CLOEXEC != O_CLOEXEC); BUILD_BUG_ON(EFD_NONBLOCK != O_NONBLOCK); if (flags & ~EFD_FLAGS_SET) return -EINVAL; ctx = kmalloc(sizeof(*ctx), GFP_KERNEL); if (!ctx) return -ENOMEM; kref_init(&ctx->kref); init_waitqueue_head(&ctx->wqh); ctx->count = count; ctx->flags = flags; ctx->id = ida_simple_get(&eventfd_ida, 0, 0, GFP_KERNEL); flags &= EFD_SHARED_FCNTL_FLAGS; flags |= O_RDWR; fd = get_unused_fd_flags(flags); if (fd < 0) goto err; file = anon_inode_getfile("[eventfd]", &eventfd_fops, ctx, flags); if (IS_ERR(file)) { put_unused_fd(fd); fd = PTR_ERR(file); goto err; } file->f_mode |= FMODE_NOWAIT; fd_install(fd, file); return fd; err: eventfd_free_ctx(ctx); return fd; }

do_eventfd比较重要的点在于anon_inode_getfile,这里通过匿名页来设置此系统调用。

再重要的就是eventfd_ctx结构,如下:

struct eventfd_ctx { struct kref kref; wait_queue_head_t wqh; /* * Every time that a write(2) is performed on an eventfd, the * value of the __u64 being written is added to "count" and a * wakeup is performed on "wqh". A read(2) will return the "count" * value to userspace, and will reset "count" to zero. The kernel * side eventfd_signal() also, adds to the "count" counter and * issue a wakeup. */ __u64 count; unsigned int flags; int id; };

这里看到了我们read和write作用的是count值,所以这个fd只能通过count来传递信息。而read/write是通过标准的fops实现,如下

static const struct file_operations eventfd_fops = { #ifdef CONFIG_PROC_FS .show_fdinfo = eventfd_show_fdinfo, #endif .release = eventfd_release, .poll = eventfd_poll, .read_iter = eventfd_read, .write = eventfd_write, .llseek = noop_llseek, };

read操作的核心实现是eventfd_ctx_do_read,这里如果是flag设置了semaphore则只会减1,否则可以直接是count值,如下:

void eventfd_ctx_do_read(struct eventfd_ctx *ctx, __u64 *cnt) { lockdep_assert_held(&ctx->wqh.lock); *cnt = ((ctx->flags & EFD_SEMAPHORE) && ctx->count) ? 1 : ctx->count; ctx->count -= *cnt; }

write操作直接在eventfd_write中,每次write会自增,也就是

ctx->count += ucnt

poll操作根据poll_wait来等待,READ_ONCE来保证count的读取只有一次,根据注释我们可以知道,wqh锁和qwh锁不会竞争问题,也就是安全的。

* poll write * ----------------- ------------ * lock ctx->wqh.lock (in poll_wait) * count = ctx->count * __add_wait_queue * unlock ctx->wqh.lock * lock ctx->qwh.lock * ctx->count += n * if (waitqueue_active) * wake_up_locked_poll * unlock ctx->qwh.lock * eventfd_poll returns 0

其代码实现如下

static unsigned int eventfd_poll(struct file *file, poll_table *wait) { struct eventfd_ctx *ctx = file->private_data; unsigned int events = 0; u64 count; poll_wait(file, &ctx->wqh, wait); count = READ_ONCE(ctx->count); if (count > 0) events |= POLLIN; if (count == ULLONG_MAX) events |= POLLERR; if (ULLONG_MAX - 1 > count) events |= POLLOUT; return events; }

而对于内核空间对eventfd的调用,可以通过eventfd_signal函数,其实现是eventfd_signal_mask,这里同样是自加如下:

__u64 eventfd_signal_mask(struct eventfd_ctx *ctx, __u64 n, unsigned mask) { unsigned long flags; /* * Deadlock or stack overflow issues can happen if we recurse here * through waitqueue wakeup handlers. If the caller users potentially * nested waitqueues with custom wakeup handlers, then it should * check eventfd_signal_count() before calling this function. If * it returns true, the eventfd_signal() call should be deferred to a * safe context. */ if (WARN_ON_ONCE(this_cpu_read(eventfd_wake_count))) return 0; spin_lock_irqsave(&ctx->wqh.lock, flags); this_cpu_inc(eventfd_wake_count); if (ULLONG_MAX - ctx->count < n) n = ULLONG_MAX - ctx->count; ctx->count += n; if (waitqueue_active(&ctx->wqh)) wake_up_locked_poll(&ctx->wqh, EPOLLIN | mask); this_cpu_dec(eventfd_wake_count); spin_unlock_irqrestore(&ctx->wqh.lock, flags); return n; }

应用测试

为了使用eventfd,我们可以直接使用c库封装的eventfd函数,示例如下:

#include <sys/eventfd.h> #include <unistd.h> #include <stdint.h> #include <stdio.h> int main() { int efd; uint64_t value; efd = eventfd(0, 0); if (efd == -1) { perror("eventfd"); return 1; } value = 1; if (write(efd, &value, sizeof(value)) == -1) { perror("write"); return 1; } if (write(efd, &value, sizeof(value)) == -1) { perror("write"); return 1; } if (read(efd, &value, sizeof(value)) == -1) { perror("read"); return 1; } printf("[kylin]: read value: %lu\n", value); close(efd); return 0; }

运行后结果如下:

[kylin]: read value: 2

至此eventfd介绍完成,详细大家在使用高性能的进程通信的时候,可以适当考虑eventfd

编辑
2025-03-29
记录知识
0

GICv3中断简介

GICv3中断是GICv2的改进,本文主要基于GICv2,简单讨论GICv3的改进内容

中断类型

我们知道GICv2默认支持如下中断类型:

  • SPI(Software Generated Interrupt) 软件生成中断,其目的是对多核之间发送的软件中断信号
  • PPI(Private Peripheral Interrupt) 私有外设中断,此中断是某个CPU核心独有的中断,如定时器
  • SPI(Shared Peripheral Interrupt) 共享外设中断,所有外设中断,可以被所有CPU响应

在GICv3中,新增了一个中断类型

  • LPI(Locality-specific Peripheral Interrupt)本地特殊外设中断,Message-Based的中断类型

中断号分配

GICv2的中断类型,如下

中断类型中断号
SGI0-15
PPI16-31
SPI32-1019
Reserved1020-1023

而GICv3的中断号相比于GICv2增加了LPI和一些扩展,如下

中断类型中断号
SGI0-15
PPI16-31
SPI32-1019
Reserved1020-8191
LPI8192-Implementation defined

ITS

为了支持Message-Based的中断LPI,GICv3的中断控制器引入了ITS组件,ITS的作用是将Device_id的Event_ID转换成INTID(LPI硬件中断号),然后通过查表转换成对应的Redistributor,最后转发给CPU上。

MSI

PCIe总线协议支持消息中断MSI,这个实现是通过GICv3的ITS实现的

编辑
2025-03-23
记录知识
0

异常

aarch64架构有多个异常方式,例如:中断,软件异常,同步异常等等

这些异常在代码中需要对应一个内存地址用作跳转,在aarch64中,每个异常都可以设置入口函数,从而实现异常的处理。与armv7不同的是,v8上默认向量表的空间更大了,支持0x80大小

linux的定义

在linux中,异常向量表通过如下定义

SYM_CODE_START(vectors) kernel_ventry 1, t, 64, sync // Synchronous EL1t kernel_ventry 1, t, 64, irq // IRQ EL1t kernel_ventry 1, t, 64, fiq // FIQ EL1t kernel_ventry 1, t, 64, error // Error EL1t kernel_ventry 1, h, 64, sync // Synchronous EL1h kernel_ventry 1, h, 64, irq // IRQ EL1h kernel_ventry 1, h, 64, fiq // FIQ EL1h kernel_ventry 1, h, 64, error // Error EL1h kernel_ventry 0, t, 64, sync // Synchronous 64-bit EL0 kernel_ventry 0, t, 64, irq // IRQ 64-bit EL0 kernel_ventry 0, t, 64, fiq // FIQ 64-bit EL0 kernel_ventry 0, t, 64, error // Error 64-bit EL0 kernel_ventry 0, t, 32, sync // Synchronous 32-bit EL0 kernel_ventry 0, t, 32, irq // IRQ 32-bit EL0 kernel_ventry 0, t, 32, fiq // FIQ 32-bit EL0 kernel_ventry 0, t, 32, error // Error 32-bit EL0 SYM_CODE_END(vectors)

通过上面可以看到,linux定义了这些异常的实现,右边有注释,就不解释了。

kernel_ventry的实现

代码如下:

.macro kernel_ventry, el:req, ht:req, regsize:req, label:req .align 7 .Lventry_start\@: .if \el == 0 /* * This must be the first instruction of the EL0 vector entries. It is * skipped by the trampoline vectors, to trigger the cleanup. */ b .Lskip_tramp_vectors_cleanup\@ .if \regsize == 64 mrs x30, tpidrro_el0 msr tpidrro_el0, xzr .else mov x30, xzr .endif .Lskip_tramp_vectors_cleanup\@: .endif sub sp, sp, #PT_REGS_SIZE #ifdef CONFIG_VMAP_STACK /* * Test whether the SP has overflowed, without corrupting a GPR. * Task and IRQ stacks are aligned so that SP & (1 << THREAD_SHIFT) * should always be zero. */ add sp, sp, x0 // sp' = sp + x0 sub x0, sp, x0 // x0' = sp' - x0 = (sp + x0) - x0 = sp tbnz x0, #THREAD_SHIFT, 0f sub x0, sp, x0 // x0'' = sp' - x0' = (sp + x0) - sp = x0 sub sp, sp, x0 // sp'' = sp' - x0 = (sp + x0) - x0 = sp b el\el\ht\()_\regsize\()_\label 0: /* * Either we've just detected an overflow, or we've taken an exception * while on the overflow stack. Either way, we won't return to * userspace, and can clobber EL0 registers to free up GPRs. */ /* Stash the original SP (minus PT_REGS_SIZE) in tpidr_el0. */ msr tpidr_el0, x0 /* Recover the original x0 value and stash it in tpidrro_el0 */ sub x0, sp, x0 msr tpidrro_el0, x0 /* Switch to the overflow stack */ adr_this_cpu sp, overflow_stack + OVERFLOW_STACK_SIZE, x0 /* * Check whether we were already on the overflow stack. This may happen * after panic() re-enables interrupts. */ mrs x0, tpidr_el0 // sp of interrupted context sub x0, sp, x0 // delta with top of overflow stack tst x0, #~(OVERFLOW_STACK_SIZE - 1) // within range? b.ne __bad_stack // no? -> bad stack pointer /* We were already on the overflow stack. Restore sp/x0 and carry on. */ sub sp, sp, x0 mrs x0, tpidrro_el0 #endif b el\el\ht\()_\regsize\()_\label .org .Lventry_start\@ + 128 // Did we overflow the ventry slot? .endm .macro tramp_alias, dst, sym, tmp mov_q \dst, TRAMP_VALIAS adr_l \tmp, \sym add \dst, \dst, \tmp adr_l \tmp, .entry.tramp.text sub \dst, \dst, \tmp .endm

这段汇编有点长,我跳过一下清理和栈必要操作,下面开始解析关键部分。

宏定义

首先查看宏定义如下

.macro kernel_ventry, el:req, ht:req, regsize:req, label:req

这里宏有四个参数,其中req指的是required,四个参数含义如下:

  • el等级
  • 是否hypervisor
  • 寄存器大小(32/64)
  • 异常类型

跳转异常

b el\el\ht\()_\regsize\()_\label

这里对应c是如下

el##${el}${ht}_${regsize}_${label}

以fiq举例如下:

kernel_ventry 1, t, 64, fi

则跳转函数el1t_64_fiq

向量入口大小

.org .Lventry_start\@ + 128

这里确保每个异常向量入口占用恰好 128 字节的空间。

确认符号

为了确定汇编宏扩展是否正确,可以通过nm查看,如下

# nm entry.o | awk '{print $3}' | grep "^el" el0t_32_error el0t_32_fiq el0t_32_irq el0t_32_sync el0t_64_error el0t_64_fiq el0t_64_irq el0t_64_sync el1h_64_error el1h_64_fiq el1h_64_irq el1h_64_sync el1t_64_error el1t_64_fiq el1t_64_irq el1t_64_sync
编辑
2025-03-21
记录知识
0

浅谈程序跑飞

我们原来最早调试51的时候,碰到程序跑飞了就不管了,那个时候认为程序跑飞是概率事件,所以没有考虑其跑飞的真正原因。最近发现aarch64架构的bl指令,非常容易导致程序跑飞。本文复现这种跑飞的情况,用来说明一个非常常见的跑飞现象,并且给出解决思路。 同样,思考以前调试51的时候,跑飞很可能是芯片的一些特性导致的问题。只怪当时知识尚浅,无法理解。

跑飞的代码

本文以rtems上的汇编为例,当然可以在linux中复现,本文就不在linux重复实验了,问题一样会出现,如下是代码diff

diff --git a/bsps/aarch64/shared/start/start.S b/bsps/aarch64/shared/start/start.S index 0a89d85035..04e2b1a8fa 100644 --- a/bsps/aarch64/shared/start/start.S +++ b/bsps/aarch64/shared/start/start.S @@ -43,6 +43,18 @@ .globl _start .section ".bsp_start_text", "ax" +kylin_call: + mov x0, xzr + ret + +kylin_out_of_control: + mov x0, xzr + bl kylin_call + mov x0, xzr + ret + /* Start entry */ _start: @@ -341,6 +353,8 @@ _start: /* Branch to start hook 1 */ bl bsp_start_hook_1 + bl kylin_out_of_control + /* Branch to boot card */ mov x0, #0 bl boot_card

上面的代码我定义了一个kylin_out_of_contrl标签,通过bl来跳转此标签,然后在kylin_out_of_control中,添加了一个没有意义的指令来代替其他指令,没有意义的指令是将x0清空。然后调用kylin_call标签,在kylin_call中继续将x0清空后返回到kylin_out_of_control,然后kylin_out_of_control内继续调用x0清空来代替其他指令,最后返回。

思考

根据我们对汇编的理解,上面的代码完全正常,bl是跳转指令,ret返回指令,mov移动指令。每个指令简单易懂,这些代码为什么组合会跑飞呢。可以思考一下

关于x30寄存器

为了理解这个问题,我们需要了解x30寄存器。在aapcs中我们可以知道x30作为lr寄存器,这个寄存器保存的是上一个函数调用pc值的+4,也就是 last_pc + 4 的值。这里出现跑飞的原因是x30寄存器不正常了。

那么我们可以思考,为什么x30会不正常?

关于ret指令

我们知道ret指令是返回指令,那么有一个问题,ret指令它会操作系统寄存器吗(x0-x30寄存器)?
aarch64的定义上,ret没有提到会操作寄存器,我们看看ret的说明如下:

image.png

根据上面的信息,我们知道ret默认返回x30寄存器。完全没问题啊,不会操作任何寄存器

那么问题就出来了,ret不会操作寄存器,但是我们bl进入的时候,会根据aapce记录x30寄存器为原pc+4。也就是会改写x30寄存器。

发现没有,x30寄存器会被aapcs改写,但是不会在ret还原。所以真相是:

如果bl出现嵌套,那么两次ret时,最外层的函数的x30和内层函数的x30相等,那么程序就一直在这里打转。于是出现了跑飞现象

演示

上面文字介绍不是很直观,下面开始演示

qemu运行rtems

如下

qemu-system-aarch64 -no-reboot -nographic -serial mon:stdio -machine xlnx-zcu102 -m 4096 -smp 4 -kernel build/aarch64/zynqmp_qemu/testsuites/libtests/kylinos.exe -s -S

挂gdb

gdb开始监测

aarch64-rtems6-gdb build/aarch64/zynqmp_qemu/testsuites/libtests/malloctest.exe

挂上断点

(gdb) b kylin_out_of_control Breakpoint 1 at 0x18008: file ../../../bsps/aarch64/shared/start/start.S, line 51. (gdb) c Continuing. Thread 1 hit Breakpoint 1, kylin_out_of_control () at ../../../bsps/aarch64/shared/start/start.S:51 51 mov x0, xzr (gdb) l 46 kylin_call: 47 mov x0, xzr 48 ret 49 50 kylin_out_of_control: 51 mov x0, xzr 52 //mov x19, x30 53 bl kylin_call 54 //mov x30, x19 55 mov x0, xzr

盯住x30寄存器

此时x30寄存器为

(gdb) x/x $x30 0x180e4 <_start+204>

这里还是一切正常,x30正好是_start+200 + 4

此时进入kylin_call,如下

(gdb) x/x $x30 0x18010 <kylin_out_of_control+8>:

然后我们第一个ret返回,如下

(gdb) s kylin_out_of_control () at ../../../bsps/aarch64/shared/start/start.S:55 55 mov x0, xzr (gdb) x/x $x30 0x18010 <kylin_out_of_control+8>: 0xaa1f03e0 (gdb) disassemble Dump of assembler code for function kylin_out_of_control: 0x0000000000018008 <+0>: mov x0, xzr 0x000000000001800c <+4>: bl 0x18000 <bsp_vector_table_begin> => 0x0000000000018010 <+8>: mov x0, xzr 0x0000000000018014 <+12>: ret End of assembler dump.

可以看到在kylin_out_of_control中了,但是异常出现了,kylin_out_of_control函数中的x30寄存器并不是我们认为的_start+204,而是kylin_out_of_control+8

按照代码逻辑,即将在0x0000000000018014处调用ret,而这个ret默认找的x30是kylin_out_of_control+8。程序会死循环了。也就是我们说的跑飞了。

确认一下x0-x30常规寄存器

为了说明这个问题,我们需要看看两次bl和ret是否是bl修改了x30,而ret不会修改任何寄存器。信息如下

第一个bl,进入kylin_out_of_control时,寄存器如下

x1 0x0 0 x2 0xffffffffffffffe8 -24 x3 0x1030c0 1061056 x4 0x103128 1061160 x5 0x4 4 x6 0x4 4 x7 0xf 15 x8 0x1c 28 x9 0x4 4 x10 0x3c0 960 x11 0x3fffffff 1073741823 x12 0x0 0 x13 0x8000000000 549755813888 x14 0x40000000 1073741824 x15 0x200000 2097152 x16 0x0 0 x17 0x0 0 x18 0x0 0 x19 0x0 0 x20 0x0 0 x21 0x0 0 x22 0x0 0 x23 0x0 0 x24 0x0 0 x25 0x0 0 x26 0x0 0 x27 0x0 0 x28 0x0 0 x29 0x0 0 x30 0x180e4 98532

然后再一个bl进入kylin_call函数,我们查看x0-x30如下

x0 0x0 0 x1 0x0 0 x2 0xffffffffffffffe8 -24 x3 0x1030c0 1061056 x4 0x103128 1061160 x5 0x4 4 x6 0x4 4 x7 0xf 15 x8 0x1c 28 x9 0x4 4 x10 0x3c0 960 x11 0x3fffffff 1073741823 x12 0x0 0 x13 0x8000000000 549755813888 x14 0x40000000 1073741824 x15 0x200000 2097152 x16 0x0 0 x17 0x0 0 x18 0x0 0 x19 0x0 0 x20 0x0 0 x21 0x0 0 x22 0x0 0 x23 0x0 0 x24 0x0 0 x25 0x0 0 x26 0x0 0 x27 0x0 0 x28 0x0 0 x29 0x0 0 x30 0x18010 98320

发现没有,bl寄存器会修改x30寄存器。

然后我们看第一个ret后的寄存器,此时应该在函数kylin_call的ret后,也就是kylin_out_of_control中

x0 0x0 0 x1 0x0 0 x2 0xffffffffffffffe8 -24 x3 0x1030c0 1061056 x4 0x103128 1061160 x5 0x4 4 x6 0x4 4 x7 0xf 15 x8 0x1c 28 x9 0x4 4 x10 0x3c0 960 x11 0x3fffffff 1073741823 x12 0x0 0 x13 0x8000000000 549755813888 x14 0x40000000 1073741824 x15 0x200000 2097152 x16 0x0 0 x17 0x0 0 x18 0x0 0 x19 0x0 0 x20 0x0 0 x21 0x0 0 x22 0x0 0 x23 0x0 0 x24 0x0 0 x25 0x0 0 x26 0x0 0 x27 0x0 0 x28 0x0 0 x29 0x0 0 x30 0x18010 98320

可以发现,此时寄存器和bl后的寄存器完全一致,x30没有发生改变

然后查看第二次ret的寄存器信息

x0 0x0 0 x1 0x0 0 x2 0xffffffffffffffe8 -24 x3 0x1030c0 1061056 x4 0x103128 1061160 x5 0x4 4 x6 0x4 4 x7 0xf 15 x8 0x1c 28 x9 0x4 4 x10 0x3c0 960 x11 0x3fffffff 1073741823 x12 0x0 0 x13 0x8000000000 549755813888 x14 0x40000000 1073741824 x15 0x200000 2097152 x16 0x0 0 x17 0x0 0 x18 0x0 0 x19 0x0 0 x20 0x0 0 x21 0x0 0 x22 0x0 0 x23 0x0 0 x24 0x0 0 x25 0x0 0 x26 0x0 0 x27 0x0 0 x28 0x0 0 x29 0x0 0 x30 0x18010 98320

完全没有变化,而且代码回到了bl后的地址上。详细如下

(gdb) disassemble Dump of assembler code for function kylin_out_of_control: 0x0000000000018008 <+0>: mov x0, xzr 0x000000000001800c <+4>: bl 0x18000 <bsp_vector_table_begin> => 0x0000000000018010 <+8>: mov x0, xzr 0x0000000000018014 <+12>: ret End of assembler dump. (gdb) s 56 ret (gdb) disassemble Dump of assembler code for function kylin_out_of_control: 0x0000000000018008 <+0>: mov x0, xzr 0x000000000001800c <+4>: bl 0x18000 <bsp_vector_table_begin> 0x0000000000018010 <+8>: mov x0, xzr => 0x0000000000018014 <+12>: ret End of assembler dump. (gdb) s 55 mov x0, xzr (gdb) disassemble Dump of assembler code for function kylin_out_of_control: 0x0000000000018008 <+0>: mov x0, xzr 0x000000000001800c <+4>: bl 0x18000 <bsp_vector_table_begin> => 0x0000000000018010 <+8>: mov x0, xzr 0x0000000000018014 <+12>: ret End of assembler dump.

修复

修复这个问题很简单,我们可以利用x19这种aapcs约定临时存储寄存器来保存x30,如下

diff --git a/bsps/aarch64/shared/start/start.S b/bsps/aarch64/shared/start/start.S index 0a89d85035..f284cf2baa 100644 --- a/bsps/aarch64/shared/start/start.S +++ b/bsps/aarch64/shared/start/start.S @@ -43,6 +43,17 @@ .globl _start .section ".bsp_start_text", "ax" +kylin_call: + mov x0, xzr + ret + +kylin_out_of_control: + mov x19, x30 + bl kylin_call + mov x30, x19 + mov x0, xzr + ret + /* Start entry */ _start: @@ -341,6 +352,8 @@ _start: /* Branch to start hook 1 */ bl bsp_start_hook_1 + bl kylin_out_of_control + /* Branch to boot card */ mov x0, #0 bl boot_card

此时运行代码,系统不再跑飞了。