编辑
2025-01-22
记录知识
0

对于安卓系统,开机起来之后会全局发送一个BOOT_COMPLETED广播。应用程序通过这个广播就知道系统正常启动完成了。通常情况下,是安卓启动完全完成之后,ams会发送一个FINISH_BOOTING_MSG消息后,开启了30s的timeout检查是否为boot-completed。这里30s是开机动画,如果开机动画正常退出或者超过30s退出,则认为boot-completed。但是,在linux中,我们的系统好像没有利用起来这个机制。

本文基于systemd的默认机制,说明一下linux如何实现boot-completed信号。

一、为什么要boot-completed信号

对于系统而言,应该主动发送boot-completed信号,让其他程序能够获取当前系统的启动状态,如何此信号接收到,则代表系统已经准备完成,ui类型的应用可以运行,可以检测系统是否破损,可以判断系统是否异常等等。

对于安卓,我们可以通过如下获取信号

getprop sys.boot_completed

二、systemd的boot_completed信号

对于systemd,默认提供了类似此机制的信号,如下:

systemctl is-system-running --wait

此方法会一直阻塞,知道systemd认为系统启动完成。引入此功能的patch如下:

https://github.com/systemd/systemd/pull/9796/commits/02d9350cda6b330669607ae88c74ac7256212741

我们可以知道,此命令一直等待的是StartupFinished的dbus信号。

当systemd的一系列jobs都正常处理完成,则发送此信号,我们可以通过list-jobs来查看jobs运行状态,如下:

systemctl list-jobs

三、应用程序如何集成

针对上面提到的,我们可以知道,我们直接监听dbus的StartupFinished信号,那么应用程序就能接收到systemd发送的boot_completed,所以我们借助yocto的工具源码dbus-wait如下:

/* * Copyright (C) 2008 Intel Corporation. * * Author: Ross Burton <ross@linux.intel.com> * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation; either version 2 of the License, or (at your option) any later * version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License along with * this program. If not, see <http://www.gnu.org/licenses/>. */ #include <string.h> #include <stdlib.h> #include <stdio.h> #include <signal.h> #include <unistd.h> #include <dbus/dbus.h> static char *interface, *member, *path; static void alarm_handler (int signum) { fprintf(stderr, "Timeout waiting for %s.%s\n", interface, member); exit (EXIT_SUCCESS); } static DBusHandlerResult filter (DBusConnection *conn, DBusMessage *message, void *user_data) { /* If the interface matches */ if (strcmp (interface, dbus_message_get_interface (message)) == 0) /* And the member matches */ if (strcmp (member, dbus_message_get_member (message)) == 0) /* And the path is NULL or matches */ if (path == NULL || strcmp (path, dbus_message_get_path (message)) == 0) /* Then exit */ exit (EXIT_SUCCESS); return DBUS_HANDLER_RESULT_HANDLED; } int main (int argc, char **argv) { DBusError error = DBUS_ERROR_INIT; DBusConnection *conn; char *match = NULL; if (argc < 3 || argc > 4) { fprintf (stderr, "$ dbus-wait <INTERFACE> <MEMBER> [PATH]\n"); return EXIT_FAILURE; } signal (SIGALRM, alarm_handler); alarm (60); /* TODO: allow system or session */ conn = dbus_bus_get (DBUS_BUS_SYSTEM, &error); if (!conn) { fprintf (stderr, "Cannot get system bus: %s\n", error.message); dbus_error_free (&error); return EXIT_FAILURE; } switch (argc) { case 3: interface = argv[1]; member = argv[2]; path = NULL; asprintf (&match, "type='signal',interface='%s',member='%s'", interface, member); break; case 4: interface = argv[1]; member = argv[2]; path = argv[3]; asprintf (&match, "type='signal',interface='%s',member='%s',path='%s'", interface, member, path); break; } dbus_bus_add_match (conn, match, &error); free (match); if (dbus_error_is_set (&error)) { fprintf (stderr, "Cannot add match: %s\n", error.message); dbus_error_free (&error); return EXIT_FAILURE; } dbus_connection_add_filter (conn, filter, NULL, NULL); while (dbus_connection_read_write_dispatch (conn, -1)) ; dbus_connection_unref (conn); return EXIT_SUCCESS; }

编译如下:

gcc dbus-wait.c $(pkg-config --libs --cflags dbus-1)-o dbus-wait

至此,我们可以直接监听此dbus信号如下:

dbus-wait org.freedesktop.systemd1.Manager StartupFinished

四、linux的缺陷

对于通常的boot_completed信号,它只能判断systemd的service是否启动完成。我们知道,对于一个桌面系统而言,systemd服务并不代表系统真实的启动完成,例如ukui-session带起来的一系列应用程序,如果peony未启动,systemd并不能察觉,StartupFinished信号的发送和peony等应用程序的是否正常运行没有产生关联。所以我们需要产生关联。

至此,我们需要在ukui-session中,一边接受此信号,一边等待系统核心应用完成。然后,构造一个完整的boot_completed信号,提供给用户,至此我们在linux中,也具备了boot_completed信号。

编辑
2025-01-22
记录知识
0

我们开发工作中,如果是面向嵌入式的场景,很多客户其实倾向于使用交叉编译工具链,我们作为操作系统可以提供与系统本身gcc版本一致的交叉编译工具,这样构建的时候就不用担心libc/binutils等版本不匹配导致的符号问题从而构建的二进制无法运行的问题,关于交叉编译工具链的文章和工具获取,可以参考交叉编译环境搭建,本文主要谈一谈关于qt工程中,如果直接使用qmake来进行交叉编译过程中,我们需要修改qt.conf和qmake.conf的一些细节性问题

一、关于qt的交叉编译

我们可以通过qt官方的wiki找到支持qt的交叉编译的办法,根据此办法,我相信大家能够很轻易的支持到qt环境的交叉编译,关于qt环境的交叉编译的细节就不再赘述,有兴趣可以仔细看看文档,文章链接如下:

https://wiki.qt.io/Cross-Compile_Qt_6_for_Raspberry_Pi

如果wiki.qt.io无法正常访问,那么github上关于qt的cross compiling的文章也是一样的意思,如下

https://github.com/UvinduW/Cross-Compiling-Qt-for-Raspberry-Pi-4

还有一篇文章可以辅助了解

https://www.stefanocottafavi.com/crosscompile-qt-for-rpi/

根据上面的信息,我简单描述如下:

  1. 获取sysroot,sysroot就是我们的系统环境
  2. 获取交叉编译工具链
  3. 下载qt的源码
  4. 在构建源码时指定CROSS_COMPILE和–sysroot
  5. 构建完成之后,此时我们的qt工程可以通过qt源码的qmake直接生成Makefile完成构建

根据上面的简述,可以知道,我们在讨论qt的交叉编译的时候,往往是通过构建了qt的源码库。从而生成了对应的qmake和相关配置,我们直接使用qmake和相关配置即可完成交叉构建。这也是qt官方推荐的做法。

但是,本文需要的是直接基于sysroot中已安装的qt版本,来手动配置qmake和qtconf来实现交叉编译,那么这可以省略编译qt源码安插–sysroot这一个步骤。与上面官方的方法不同的点我列表如下:

qt官方:需要构建qt源码工程

本文讨论的:无需构建qt源码工程,程序链接sysroot下的qt库文件

二、实现思路

我们知道如果不想通过重新构建qt源码来实现交叉编译的话,我们就需要在host上强行指定好sysroot,头文件位置,库文件位置,数据文件位置等。这需要对qmake和qt.conf有详细的了解。这也是本文讨论的重点。主要步骤如下:

2.1 下载交叉编译工具链

我们可以通过linaro官网上下载工具链,也可以使用我们提供的工具链,交叉编译环境搭建有提到工具链地址是

gcc-kylin-9.3.0-2024.10-x86_64_aarch64-linux-gnu.tar.gz 文件获取位置(110服务器):/home/yangquan/develop/嵌入式相关/01_嵌入式定制部门/嵌入式版本项目&产品版本问题解决集/KylinSdk

2.2 下载sysroot环境

我们需要一个编译的sysroot环境,这个环境可以在已有的机器环境上打包其rootfs,也可以在麒麟平台上根据系统iso的filesystem.squashfs来进行解压获取,里面是一个标准的操作系统环境,我们还需要在这个环境中根据deb包来下载安装必要的头文件和动态库

2.3 配置qmake

qt默认提供了基于aarch64的交叉编译配置,地址如下:

/usr/lib/x86_64-linux-gnu/qt5/mkspecs/linux-aarch64-gnu-g++/qmake.conf

我们可以发现其会设置相应的环境变量,例如

QMAKE_CC = aarch64-linux-gnu-gcc QMAKE_CXX = aarch64-linux-gnu-g++ QMAKE_LINK = aarch64-linux-gnu-g++ QMAKE_LINK_SHLIB = aarch64-linux-gnu-g++ # modifications to linux.conf QMAKE_AR = aarch64-linux-gnu-ar cqs QMAKE_OBJCOPY = aarch64-linux-gnu-objcopy QMAKE_NM = aarch64-linux-gnu-nm -P QMAKE_STRIP = aarch64-linux-gnu-strip

可以发现,这里指向的是交叉编译工具链中的编译工具

2.4 配置qt.conf

我们配置了qmake默认使用交叉编译工具之后,还需要配置qt.conf,如下:

[Paths] Prefix=/usr ArchData=lib/aarch64-linux-gnu/qt5 Binaries=lib/qt5/bin Data=share/qt5 TargetSpec=linux-aarch64-gnu-g++ HostSpec=linux-aarch64-gnu-g++ Documentation=share/qt5/doc Examples=lib/aarch64-linux-gnu/qt5/examples Headers=include/aarch64-linux-gnu/qt5 HostBinaries=lib/qt5/bin HostData=lib/x86_64-linux-gnu/qt5 HostLibraries=lib/x86_64-linux-gnu Imports=lib/aarch64-linux-gnu/qt5/imports Libraries=lib/aarch64-linux-gnu LibraryExecutables=lib/aarch64-linux-gnu/qt5/libexec Plugins=lib/aarch64-linux-gnu/qt5/plugins Qml2Imports=lib/aarch64-linux-gnu/qt5/qml Settings=/etc/xdg Translations=share/qt5/translations Sysroot=/root/tf/kylin_test_cross/sysroot SysrootifyPrefix=true

例如sysroot位置,header文件查找位置,libraries库查找位置。

有了这个qt.conf,我们的一些环境变量会发生改变,例如,如果没有qt.conf,我们query的信息如下:

# qmake -query QT_SYSROOT: QT_INSTALL_PREFIX:/usr QT_INSTALL_ARCHDATA:/usr/lib/x86_64-linux-gnu/qt5 QT_INSTALL_DATA:/usr/share/qt5 QT_INSTALL_DOCS:/usr/share/qt5/doc QT_INSTALL_HEADERS:/usr/include/x86_64-linux-gnu/qt5 QT_INSTALL_LIBS:/usr/lib/x86_64-linux-gnu QT_INSTALL_LIBEXECS:/usr/lib/x86_64-linux-gnu/qt5/libexec QT_INSTALL_BINS:/usr/lib/qt5/bin QT_INSTALL_TESTS:/usr/tests QT_INSTALL_PLUGINS:/usr/lib/x86_64-linux-gnu/qt5/plugins QT_INSTALL_IMPORTS:/usr/lib/x86_64-linux-gnu/qt5/imports QT_INSTALL_QML:/usr/lib/x86_64-linux-gnu/qt5/qml QT_INSTALL_TRANSLATIONS:/usr/share/qt5/translations QT_INSTALL_CONFIGURATION:/etc/xdg QT_INSTALL_EXAMPLES:/usr/lib/x86_64-linux-gnu/qt5/examples QT_INSTALL_DEMOS:/usr/lib/x86_64-linux-gnu/qt5/examples QT_HOST_PREFIX:/usr QT_HOST_DATA:/usr/lib/x86_64-linux-gnu/qt5 QT_HOST_BINS:/usr/lib/qt5/bin QT_HOST_LIBS:/usr/lib/x86_64-linux-gnu QMAKE_SPEC:linux-g++ QMAKE_XSPEC:linux-g++ QMAKE_VERSION:3.1 QT_VERSION:5.12.8

可以发现其默认指向了本地的库和头文件,并未设置sysroot地址,如果我们指定qt.conf,则如下

# qmake -query -qtconf ./qt.conf QT_SYSROOT:/root/tf/kylin_test_cross/sysroot/ QT_SYSROOT:/root/tf/kylin_test_cross/sysroot QT_INSTALL_PREFIX:/root/tf/kylin_test_cross/sysroot/usr QT_INSTALL_PREFIX/raw:/usr QT_INSTALL_ARCHDATA:/root/tf/kylin_test_cross/sysroot/usr/lib/aarch64-linux-gnu/qt5 QT_INSTALL_ARCHDATA/raw:/usr/lib/aarch64-linux-gnu/qt5 QT_INSTALL_DATA:/root/tf/kylin_test_cross/sysroot/usr/share/qt5 QT_INSTALL_DATA/raw:/usr/share/qt5 QT_INSTALL_DOCS:/root/tf/kylin_test_cross/sysroot/usr/share/qt5/doc QT_INSTALL_DOCS/raw:/usr/share/qt5/doc QT_INSTALL_HEADERS:/root/tf/kylin_test_cross/sysroot/usr/include/aarch64-linux-gnu/qt5 QT_INSTALL_HEADERS/raw:/usr/include/aarch64-linux-gnu/qt5 QT_INSTALL_LIBS:/root/tf/kylin_test_cross/sysroot/usr/lib/aarch64-linux-gnu QT_INSTALL_LIBS/raw:/usr/lib/aarch64-linux-gnu QT_INSTALL_LIBEXECS:/root/tf/kylin_test_cross/sysroot/usr/lib/aarch64-linux-gnu/qt5/libexec QT_INSTALL_LIBEXECS/raw:/usr/lib/aarch64-linux-gnu/qt5/libexec QT_INSTALL_BINS:/root/tf/kylin_test_cross/sysroot/usr/lib/qt5/bin QT_INSTALL_BINS/raw:/usr/lib/qt5/bin QT_INSTALL_TESTS:/root/tf/kylin_test_cross/sysroot/usr/tests QT_INSTALL_TESTS/raw:/usr/tests QT_INSTALL_PLUGINS:/root/tf/kylin_test_cross/sysroot/usr/lib/aarch64-linux-gnu/qt5/plugins QT_INSTALL_PLUGINS/raw:/usr/lib/aarch64-linux-gnu/qt5/plugins QT_INSTALL_IMPORTS:/root/tf/kylin_test_cross/sysroot/usr/lib/aarch64-linux-gnu/qt5/imports QT_INSTALL_IMPORTS/raw:/usr/lib/aarch64-linux-gnu/qt5/imports QT_INSTALL_QML:/root/tf/kylin_test_cross/sysroot/usr/lib/aarch64-linux-gnu/qt5/qml QT_INSTALL_QML/raw:/usr/lib/aarch64-linux-gnu/qt5/qml QT_INSTALL_TRANSLATIONS:/root/tf/kylin_test_cross/sysroot/usr/share/qt5/translations QT_INSTALL_TRANSLATIONS/raw:/usr/share/qt5/translations QT_INSTALL_CONFIGURATION:/root/tf/kylin_test_cross/sysroot/etc/xdg QT_INSTALL_CONFIGURATION/raw:/etc/xdg QT_INSTALL_EXAMPLES:/root/tf/kylin_test_cross/sysroot/usr/lib/aarch64-linux-gnu/qt5/examples QT_INSTALL_EXAMPLES/raw:/usr/lib/aarch64-linux-gnu/qt5/examples QT_INSTALL_DEMOS:/root/tf/kylin_test_cross/sysroot/usr/lib/aarch64-linux-gnu/qt5/examples QT_INSTALL_DEMOS/raw:/usr/lib/aarch64-linux-gnu/qt5/examples QT_HOST_PREFIX:/usr QT_HOST_DATA:/usr/lib/x86_64-linux-gnu/qt5 QT_HOST_BINS:/usr/lib/qt5/bin QT_HOST_LIBS:/usr/lib/x86_64-linux-gnu QMAKE_SPEC:linux-aarch64-gnu-g++ QMAKE_XSPEC:linux-aarch64-gnu-g++ QMAKE_VERSION:3.1 QT_VERSION:5.12.8

可以发现,我们headers和libs等等都从正确的路径寻找。

至此,我们发现,整个qt.conf和qmake.conf已经正确配置,我们可以良好的进行编译构建。

三、演示

如果对于一个qt仓库,我们以qt的example为例,/usr/lib/x86_64-linux-gnu/qt5/examples/gui/analogclock。我们可以如下:

cd /usr/lib/x86_64-linux-gnu/qt5/examples/gui/analogclock

我们需要根据qmake生成Makefile,如下

/usr/lib/qt5/bin/qmake -qtconf ./qt.conf

然后直接make,如下:

make

此时,我们会生成如下文件

# file analogclock analogclock: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, BuildID[sha1]=5f848734c8f59a6958bf829be3a72dd4d6b0a124, for GNU/Linux 3.7.0, not stripped

此时,文件生成完成,可以发现是aarch64的qt程序,我们将其发送到实际机器上即可正常运行。

编辑
2025-01-22
记录知识
0

因为客户的需求,我们需要在标准linux上支持c,d盘的基本功能,这样用户在安装程序时,需要可以将程序安装在D盘,这样从某种意义上来说达到了用户和系统的应用程序隔离。本文提供一种思路,用于设计C,D盘的设计,基于此思路的衍生,可以完全实施C,D盘的基本功能

一、相关补丁

为了将补丁更突出出来,这里第一时间将补丁贴出,如下:

From 75d9bc4ee5c2b6b2b0a7efa7beed7c2b8cfe51e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=94=90=E5=B3=B0?= <tangfeng@kylinos.cn> Date: Wed, 14 Aug 2024 12:17:16 +0800 Subject: [PATCH] support appfs for C/D disk export USE_APPFS marco to open this feature --- src/archives.c | 155 ++++++++++++++++++++++++++++++++++++++++++++++++- src/remove.c | 59 +++++++++++++++++++ 2 files changed, 213 insertions(+), 1 deletion(-) diff --git a/src/archives.c b/src/archives.c index fe4d8a9..bca81b5 100644 --- a/src/archives.c +++ b/src/archives.c @@ -387,6 +387,65 @@ does_replace(struct pkginfo *new_pkg, struct pkgbin *new_pkgbin, return false; } +static void copy_file(const char *src, const char *dest) { + struct stat stat_buf; + int sfd, dfd; + char *buf; + int buf_read = 0; + int buf_write = 0; + struct timespec times[2]; + + if (!strcmp(src, dest)) + return; + + sfd = open(src, O_RDONLY); + dfd = open(dest, O_WRONLY | O_CREAT | O_TRUNC, stat_buf.st_mode); + if (sfd < 0 || dfd < 0) { + return ; + } + + stat(src, &stat_buf); + + buf = (char *) malloc(4096); + while ((buf_read = read(sfd, buf, 4096))) { + buf_write = write(dfd, buf, buf_read); + if(buf_write != buf_read){ + } + } + free(buf); + + if (buf_read < 0) { + printf("buf_read le 0\n"); + } + + fchmod(dfd, stat_buf.st_mode); + if(fchown(dfd, stat_buf.st_uid, stat_buf.st_gid)){ + printf("change owner failed\n"); + } + + times[0] = stat_buf.st_atim; + times[1] = stat_buf.st_mtim; + futimens(dfd, times); + + close(sfd); + close(dfd); +} + +#define USE_APPFS +static bool get_useappfs(void) +{ + static bool use_appfs = false; + +#ifdef USE_APPFS + const char *env = getenv("USE_APPFS"); + if(env) + use_appfs = true; + else + use_appfs = false; +#endif + return use_appfs; +} + static void tarobject_extract(struct tarcontext *tc, struct tar_entry *te, const char *path, struct file_stat *st, @@ -401,6 +460,9 @@ tarobject_extract(struct tarcontext *tc, struct tar_entry *te, char fnamenewbuf[256]; char *newhash; int rc; + char appfs[256]; + + snprintf(appfs, 256, "%s%s", appfs_prefix, path); switch (te->type) { case TAR_FILETYPE_FILE: @@ -450,10 +512,18 @@ tarobject_extract(struct tarcontext *tc, struct tar_entry *te, pop_cleanup(ehflag_normaltidy); /* fd = open(path) */ if (close(fd)) ohshite(_("error closing/writing '%.255s'"), te->name); + + /* copy file to appfs */ + if(get_useappfs()) + copy_file(path, appfs); break; case TAR_FILETYPE_FIFO: if (mkfifo(path, 0)) ohshite(_("error creating pipe '%.255s'"), te->name); + + /* mkfifo at appfs */ + if(get_useappfs()) + mkfifo(appfs, 0); debug(dbg_eachfiledetail, "tarobject fifo"); break; case TAR_FILETYPE_CHARDEV: @@ -477,6 +547,13 @@ tarobject_extract(struct tarcontext *tc, struct tar_entry *te, varbuf_end_str(&hardlinkfn); if (link(hardlinkfn.buf, path)) ohshite(_("error creating hard link '%.255s'"), te->name); + + /* hardlink at appfs */ + if(get_useappfs()){ + if(link(hardlinkfn.buf, appfs)) + printf("make hardlink on %s failed\n", appfs); + } + namenode->newhash = linknode->newhash; debug(dbg_eachfiledetail, "tarobject hardlink hash=%s", namenode->newhash); break; @@ -484,12 +561,32 @@ tarobject_extract(struct tarcontext *tc, struct tar_entry *te, /* We've already checked for an existing directory. */ if (symlink(te->linkname, path)) ohshite(_("error creating symbolic link '%.255s'"), te->name); + + /* symlink at appfs */ + if(get_useappfs()) { + /* this symlink maybe fail */ + if (symlink(te->linkname, appfs)){ + unlink(appfs); + if(symlink(te->linkname, appfs)){ + printf("make symlink on %s failed\n", appfs); + } + } + } + debug(dbg_eachfiledetail, "tarobject symlink creating"); break; case TAR_FILETYPE_DIR: /* We've already checked for an existing directory. */ if (mkdir(path, 0)) ohshite(_("error creating directory '%.255s'"), te->name); + + /* create directory on appfs */ + if(get_useappfs()){ + struct stat stab; + if (!(stat(appfs, &stab) == 0 && S_ISDIR(stab.st_mode))) + mkdir(appfs, 0); + } + debug(dbg_eachfiledetail, "tarobject directory creating"); break; default: @@ -808,6 +905,14 @@ tarobject(struct tar_archive *tar, struct tar_entry *ti) "installing another version"), ti->name); debug(dbg_eachfiledetail,"tarobject nonexistent"); } else { + /* rename file on appfs */ + if(get_useappfs()){ + char appfsnew[256]; + char appfs[256]; + snprintf(appfsnew, 256, "%s%s", appfs_prefix, fnamenewvb.buf); + snprintf(appfs, 256, "%s%s", appfs_prefix, fnamevb.buf); + rename(appfsnew, appfs); + } debug(dbg_eachfiledetail,"tarobject restored tmp to main"); statr= lstat(fnamevb.buf,&stab); if (statr) @@ -838,6 +943,14 @@ tarobject(struct tar_archive *tar, struct tar_entry *ti) if (!stat(fnamevb.buf,&stabtmp) && S_ISDIR(stabtmp.st_mode)) { debug(dbg_eachfiledetail, "tarobject directory exists"); existingdir = true; + /* mkdir on appfs */ + if(get_useappfs()){ + char appfs[256]; + snprintf(appfs, 256, "%s%s", appfs_prefix, fnamevb.buf); + if (!(!stat(appfs,&stabtmp) && S_ISDIR(stabtmp.st_mode))) { + mkdir(appfs, S_IRWXU|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH); + } + } } break; case TAR_FILETYPE_FILE: @@ -1058,6 +1171,17 @@ tarobject(struct tar_archive *tar, struct tar_entry *ti) if (nifd->namenode->flags & FNNF_NEW_CONFF) { debug(dbg_conffdetail,"tarobject conffile extracted"); nifd->namenode->flags |= FNNF_ELIDE_OTHER_LISTS; + + if(get_useappfs()){ + char appfsnew[256]; + char appfs[256]; + snprintf(appfsnew, 256, "%s%s", appfs_prefix, fnamenewvb.buf); + snprintf(appfs, 256, "%s%s", appfs_prefix, fnamevb.buf); + if(S_ISDIR(stab.st_mode) || S_ISREG(stab.st_mode)){ + rename(appfsnew, appfs); + } + } + return 0; } @@ -1120,7 +1244,16 @@ tarobject(struct tar_archive *tar, struct tar_entry *ti) } else { if (rename(fnamenewvb.buf, fnamevb.buf)) ohshite(_("unable to install new version of '%.255s'"), ti->name); - + /* rename on appfs */ + if(get_useappfs()){ + char appfsnew[256]; + char appfs[256]; + snprintf(appfsnew, 256, "%s%s", appfs_prefix, fnamenewvb.buf); + snprintf(appfs, 256, "%s%s", appfs_prefix, fnamevb.buf); + if(access(appfs, F_OK)){ + rename(appfsnew, appfs); + } + } /* * CLEANUP: Now the new file is in the destination file, and the * old file is in .dpkg-tmp to be cleaned up later. We now need @@ -1306,6 +1439,14 @@ tar_deferred_extract(struct fsys_namenode_list *files, struct pkginfo *pkg) ohshite(_("unable to install new version of '%.255s'"), cfile->namenode->name); + /* rename on appfs */ + if(get_useappfs()) { + char appfsnew[256]; + char appfs[256]; + snprintf(appfsnew, 256, "%s%s", appfs_prefix, fnamenewvb.buf); + snprintf(appfs, 256, "%s%s", appfs_prefix, fnamevb.buf); + rename(appfsnew, appfs); + } if(kysec_whlist_exectl_multi_add_for_dpkg == NULL) { if(kysec_whlist_exectl_add_for_dpkg != NULL) @@ -1750,6 +1891,18 @@ archivefiles(const char *const *argv) ohshit(_("archive '%s' is not a regular file"), argp[i]); } + /* check appfs directory exist */ + if(get_useappfs()){ + if(access(appfs_prefix, F_OK)){ + struct stat st; + stat(appfs_prefix, &st); + if(!S_ISDIR(st.st_mode)){ + mkdir(appfs_prefix, 0); + chmod(appfs_prefix, S_IRWXU|S_IRWXG|S_IRWXO); + } + } + } + currenttime = time(NULL); /* Initialize fname variables contents. */ diff --git a/src/remove.c b/src/remove.c index b8727b6..d24f21b 100644 --- a/src/remove.c +++ b/src/remove.c @@ -551,6 +551,21 @@ removal_bulk_file_is_shared(struct pkginfo *pkg, struct fsys_namenode *namenode) return shared; } +#define USE_APPFS +static bool get_useappfs(void) +{ + static bool use_appfs = false; + +#ifdef USE_APPFS + const char *env = getenv("USE_APPFS"); + if(env) + use_appfs = true; + else + use_appfs = false; +#endif + return use_appfs; +} + static void removal_bulk_remove_files(struct pkginfo *pkg) { @@ -560,6 +575,7 @@ removal_bulk_remove_files(struct pkginfo *pkg) static struct varbuf fnvb; struct varbuf_state fnvb_state; struct stat stab; + char appfs[256]; pkg_set_status(pkg, PKG_STAT_HALFINSTALLED); modstatdb_note(pkg); @@ -584,6 +600,10 @@ removal_bulk_remove_files(struct pkginfo *pkg) is_dir = stat(fnvb.buf, &stab) == 0 && S_ISDIR(stab.st_mode); + if(get_useappfs()){ + snprintf(appfs, 256, "%s%s", appfs_prefix, fnvb.buf); + } + /* A pkgset can share files between its instances that we * don't want to remove, we just want to forget them. This * applies to shared conffiles too. */ @@ -593,6 +613,11 @@ removal_bulk_remove_files(struct pkginfo *pkg) /* Non-shared conffiles are kept. */ if (namenode->flags & FNNF_OLD_CONFF) { push_leftover(&leftover, namenode); + + /* unlink non-shared conffiles file */ + if(get_useappfs()) + secure_unlink(appfs); + continue; } @@ -638,6 +663,19 @@ removal_bulk_remove_files(struct pkginfo *pkg) varbuf_end_str(&fnvb); debug(dbg_eachfiledetail, "removal_bulk removing '%s'", fnvb.buf); + + /* remove appfs dir */ + if(get_useappfs()){ + if (stat(appfs, &stab) == 0 && S_ISDIR(stab.st_mode)){ + /* ignore /$appfs/. directory */ + char prefix[256]; + snprintf(prefix, 256, "%s%s", appfs_prefix, "/."); + if (strcmp(appfs, prefix) == 0) { + continue; + } + rmdir(appfs); + } + } if (!rmdir(fnvb.buf) || errno == ENOENT || errno == ELOOP) continue; if (errno == ENOTEMPTY || errno == EEXIST) { debug(dbg_eachfiledetail, @@ -657,6 +695,12 @@ removal_bulk_remove_files(struct pkginfo *pkg) debug(dbg_eachfiledetail, "removal_bulk unlinking '%s'", fnvb.buf); if (secure_unlink(fnvb.buf)) ohshite(_("unable to securely remove '%.250s'"), fnvb.buf); + + /* delete appfs file */ + if(get_useappfs()){ + if((access(appfs, F_OK)==0) || (lstat(appfs, &stab) == 0 && S_ISLNK(stab.st_mode))) + secure_unlink(appfs); + } } write_filelist_except(pkg, &pkg->installed, leftover, 0); maintscript_installed(pkg, POSTRMFILE, "post-removal", "remove", NULL); @@ -681,6 +725,7 @@ static void removal_bulk_remove_leftover_dirs(struct pkginfo *pkg) { struct fsys_namenode *namenode; static struct varbuf fnvb; struct stat stab; + char appfs[256]; /* We may have modified this previously. */ ensure_packagefiles_available(pkg); @@ -710,6 +755,10 @@ static void removal_bulk_remove_leftover_dirs(struct pkginfo *pkg) { varbuf_add_str(&fnvb, usenode->name); varbuf_end_str(&fnvb); + if(get_useappfs()){ + snprintf(appfs, 256, "%s%s", appfs_prefix, fnvb.buf); + } + if (!stat(fnvb.buf,&stab) && S_ISDIR(stab.st_mode)) { debug(dbg_eachfiledetail, "removal_bulk is a directory"); /* Only delete a directory or a link to one if we're the only @@ -733,6 +782,16 @@ static void removal_bulk_remove_leftover_dirs(struct pkginfo *pkg) { trig_path_activate(usenode, pkg); debug(dbg_eachfiledetail, "removal_bulk removing '%s'", fnvb.buf); + /* remove appfs file */ + if(get_useappfs()){ + if (!stat(appfs, &stab) && S_ISDIR(stab.st_mode)) { + rmdir(appfs); + } + if (lstat(appfs, &stab) == 0 && S_ISLNK(stab.st_mode)) { + unlink(appfs); + } + } + if (!rmdir(fnvb.buf) || errno == ENOENT || errno == ELOOP) continue; if (errno == ENOTEMPTY || errno == EEXIST) { warning(_("while removing %.250s, directory '%.250s' not empty so not removed"), -- 2.25.1

二、现状

2.1 默认安装包放在根目录

我们可以知道,linux默认情况下将so和elf程序放在/usr/bin和/usr/lib下,与系统核心库不做区分。这样用户在安装deb包的时候,也会默认在/usr/bin和/usr/lib中安装,这就导致了所有的应用软件包都安装在系统核心目录下。

同样的,我们知道,windows默认将应用程序和dll安装的c盘,但是应用程序可以选择默认安装环境,这里可以手动指定为D盘。其差别如下:

image.png

这里需要明确的是windows核心的系统库,例如vs c++等相关库,只能安装在C盘。

根据此现状,Linux系统需要提供一个功能,让其应用程序可以安装在D盘等类似的盘符

2.2 长期以来的生态依赖根目录,而不是其他目录

我们知道麒麟系统的包生态基于deb,虽然现在存在开明包格式,但是为了不改变太多,默认deb包格式的方式短期内不能抛弃。

而deb包默认将其安装的根目录,设置其preinstall,postinstall,preremove,postremove都需要在根目录运行。

并且,dpkg管理的包列表默认存放在/var/lib/dpkg/的list中,而这里的list记录了实际的文件内容,举例如下:

root@kylin:~# cat /var/lib/dpkg/info/libhw265dec.list /. /etc /etc/dbus-1 /etc/dbus-1/system.d /etc/dbus-1/system.d/com.huawei.dassistant.conf /usr /usr/bin /usr/bin/DAssistantd /usr/bin/qs-smc-module-installer.sh /usr/include /usr/include/hwd_api.h /usr/lib /usr/lib/libhw265dec.so /usr/lib/pkgconfig /usr/lib/pkgconfig/hw265dec.pc /usr/lib/systemd /usr/lib/systemd/system /usr/lib/systemd/system/qs-smc-module-installer.service /usr/share /usr/share/doc /usr/share/doc/libhw265dec /usr/share/doc/libhw265dec/changelog.gz

所以,如果我们一味的将deb包安装在其他目录,则问题太多,太开放,解决问题不可实现。

2.3 总结

根据上述2.1 和 2.2 的现状,我们可以知道,我们长期依赖的ubuntu 系列的生态,导致我们没有办法轻易的将系统划分为单独的C盘和D盘。

三、解决思路

为了解决这个问题,我们可以两个思路

  • 弃用ubuntu生态,使用优秀全新的设计,例如开明包格式
  • 遵循ubuntu生态,以冗余换兼容

3.1 不破不立

麒麟系统集成了ubuntu和debian系列的应用生态,所以deb包格式能够直接安装,而正是因为如此,deb包在设计的时候没有考虑C,D盘的划分。所以我们没办法直接划分C和D盘。

所以我们需要抛弃这块的生态,重新定义包的安装方式。例如开明包格式,如下:

https://gitlab2.kylin.com/lixinyue/kaiming-design-docs/-/tree/master/%E5%BC%80%E6%98%8E%E7%94%A8%E6%88%B7%E6%93%8D%E4%BD%9C%E6%89%8B%E5%86%8C

其每个应用程序都需要遵循开明包格式发布,而开明包自带沙箱隔离机制,故实现C/D盘将十分简单,当前成果状态如下:

image.png

但此文档主要目的不是讨论此方案

3.2 多方兼顾

为了兼容ubuntu/debian系列的安装方式,又体现C/D盘的基本功能,我们必须两方都兼顾,所以我们实施的方案如下:

image.png

这样,程序被默认安装在根,并且原封不动安装在appfs中,此时应用程序的安装可以在appfs中体现,而根的文件,我们可以通过dm-snapshot实现在大更新的时候进行merge操作。从而完成系统的整体更新,关于dm-snapshot的操作,可参考其他文章。

四、设计方法

4.1 修改dpkg

对于ubuntu/debian系统的软件包,均是通过apt来安装的deb包,而实际解包安装的动作是dpkg实现。所以我们需要基于dpkg来进行修改。

4.2 安装过程

针对src/archives.c文件,我们需要将在deb包的tar包解压过程中,执行tarobject_extract时,对于普通文件进行copy,目录进程创建,对于fifo/link/symlink进行重建,如下

image.png

4.3 卸载过程

针对src/remove.c文件,我们需要将在deb包卸载删除过程中,执行removal_bulk时,对普通文件和目录进行删除,对fifo/link/symlink进行unlink,如下

image.png

4.4 如何启用

默认情况下,我们不启用此功能,如果想要启用,则可以导出环境变量即可,如下:

export USE_APPFS=1 对于代码,实现如下: #define USE_APPFS static bool get_useappfs(void) { static bool use_appfs = false; #ifdef USE_APPFS const char *env = getenv("USE_APPFS"); if(env) use_appfs = true; else use_appfs = false; #endif return use_appfs; }

五、体验

这里我们假设系统支持了此功能,那么如果我们想要启动appfs的基本功能,如下:

export USE_APPFS=1

然后,我们正常通过apt/dpkg 安装应用,安装之前,我们确认没有appfs,如下:

root@kylin:~# file /appfs /appfs: cannot open `/appfs' (No such file or directory)

然后我们正常安装程序,这里以tree为例

root@kylin:~# apt install tree 正在读取软件包列表... 完成 正在分析软件包的依赖关系树 正在读取状态信息... 完成 下列软件包是自动安装的并且现在不需要了: libutempter0 python3-click python3-colorama python3-itsdangerous python3-jinja2 python3-markupsafe 使用'apt autoremove'来卸载它(它们)。 下列【新】软件包将被安装: tree 升级了 0 个软件包,新安装了 1 个软件包,要卸载 0 个软件包,有 14 个软件包未被升级。 需要下载 44.5 kB 的归档。 解压缩后会消耗 111 kB 的额外空间。 获取:1 http://archive.kylinos.cn/kylin/KYLIN-ALL 10.1-rk3588/universe arm64 tree arm64 1.8.0-1 [44.5 kB] 已下载 44.5 kB,耗时 0秒 (262 kB/s) 正在选中未选择的软件包 tree。 (正在读取数据库 ... 系统当前共安装有 210448 个文件和目录。) 准备解压 .../tree_1.8.0-1_arm64.deb ... 正在解压 tree (1.8.0-1) ... 正在设置 tree (1.8.0-1) ... 正在处理用于 man-db (2.9.1-1kylin0k1.0) 的触发器 ... 正在处理用于 kysec-utils (3.3.6.1-0k8.18) 的触发器 ...

此时我们可以发现,tree在appfs中存在,如下

root@kylin:~# find /appfs/ /appfs/ /appfs/usr /appfs/usr/bin /appfs/usr/bin/tree /appfs/usr/share /appfs/usr/share/doc /appfs/usr/share/doc/tree /appfs/usr/share/doc/tree/README.gz /appfs/usr/share/doc/tree/changelog.Debian.gz /appfs/usr/share/doc/tree/copyright /appfs/usr/share/doc/tree/TODO /appfs/usr/share/man /appfs/usr/share/man/man1 /appfs/usr/share/man/man1/tree.1.gz

如果我们想要使用appfs中的tree,我们可以直接运行,如果程序带so,则我们修改ldconfig即可,如下

LD_LIBRARY_PATH=/appfs/pathtolibrary/ exe

此时如果我们卸载tree,如下:

root@kylin:~# dpkg -P tree (正在读取数据库 ... 系统当前共安装有 210455 个文件和目录。) 正在卸载 tree (1.8.0-1) ... 正在处理用于 man-db (2.9.1-1kylin0k1.0) 的触发器 ... 正在处理用于 kysec-utils (3.3.6.1-0k8.18) 的触发器 ...

此时我们可以看到appfs的文件均被删除,如下

root@kylin:~# find /appfs/ /appfs/ /appfs/usr /appfs/usr/bin /appfs/usr/share /appfs/usr/share/doc /appfs/usr/share/man /appfs/usr/share/man/man1

这里遗留了几个文件夹没有删除,因为这几个文件夹不是tree包带来的,所以没有删除是正常的

windows在安装包后卸载,D盘也是遗留空文件夹。这不是异常。

编辑
2025-01-22
记录知识
0

咱们有一个基本的面试题,有些同事可能存在一些误解的情况,本文基于这个题目介绍一下关于aarch64的情况

一、关于题目

请说明计数寄存器(PC)和堆栈寄存器(SP)以及链接寄存器(LR)的作用 这个题目很清楚,需要阐述PC,SP和LR的作用,那不清晰的点在哪里呢

二、存在歧义的点

如果此问题是arm系列芯片,也就是32位,那么我们这里描述的是

R13,R14,R15

如下:

image.png 但是如果是在aarch64系列芯片,也就是64位,那么我们这里描述的是

X29,X30

如下:

image.png 值得注意的是,aarch64没有单独的PC寄存器。

主要原因如下,可以从armv8的spec中找到:

The current Program Counter (PC) cannot be referred to by number as if part of the general register file and therefore cannot be used as the source or destination of arithmetic instructions, or as the base, index or transfer register of load and store instructions. The only instructions that read the PC are those whose function it is to compute a PC-relative address (ADR, ADRP, literal load, and direct branches), and the branch-and-link instructions that store a return address in the link register (BL and BLR). The only way to modify the program counter is using branch, exception generation and exception return instructions. Where the PC is read by an instruction to compute a PC-relative address, then its value is the address of that instruction. Unlike A32 and T32, there is no implied offset of 4 or 8 bytes.

这里我们知道三个信息:

  • aarch64下的pc寄存器不是通用寄存器
  • pc寄存器可以通过adr,adrp,ldr这种加载指令,或者在代码分支内部直接计算偏移
  • 修改pc寄存器可以通过bl,blr这类跳转指令直接修改

也就是说,在aarch64上,pc寄存器可以在函数上可以直接通过偏移计算出来,或者通过adr等加载指令来计算偏移获取pc的值,而跳转可以通过bl来修改pc指针。

至此,我们从官方渠道了解了aarch64的关于pc寄存器的歧义点。

而实际上,在aarch64平台,我们上层使用过程中,仍是可以轻松的获取这些寄存器的值,和arm32相差无几,接下来我简单介绍一下在aarch64平台上这三个寄存器

三、示例

3.1 SP寄存器

aarch64的SP寄存器根据异常等级区分,也就是当前owner是ELn,那么SP就是SP_ELn。

我们可以如下示例:

(gdb) info register sp sp 0x7fffffefe0 0x7fffffefe0

sp表示当前函数的堆栈指针,它和x29寄存器是相等的,如下:

(gdb) p/x $x29 $2 = 0x7fffffefe0

对于一个函数,我们在函数的开始,会看到sp会被修改,如下:

0x0000000000400750 <+0>: stp x29, x30, [sp, #-48]!

可以发现sp变小,也就是说,上一级的sp地址应该是sp+48,如下

(gdb) p $sp+48 $5 = (void *) 0x7ffffff010

此时我们到上一级函数load_data中,可以查看如下:

(gdb) p/x $sp $1 = 0x7ffffff010

可以看汇编知道接下来会调用test函数

(gdb) disassemble Dump of assembler code for function load_data: 0x0000000000400674 <+0>: stp x29, x30, [sp, #-48]! 0x0000000000400678 <+4>: mov x29, sp 0x000000000040067c <+8>: str x0, [sp, #24] => 0x0000000000400680 <+12>: ldr x0, [sp, #24] 0x0000000000400684 <+16>: str x0, [sp, #40] 0x0000000000400688 <+20>: ldr x0, [sp, #40] 0x000000000040068c <+24>: ldr x2, [x0] 0x0000000000400690 <+28>: ldr w1, [x0, #8] 0x0000000000400694 <+32>: mov x0, x2 0x0000000000400698 <+36>: bl 0x400750 <test> 0x000000000040069c <+40>: nop 0x00000000004006a0 <+44>: ldp x29, x30, [sp], #48 0x00000000004006a4 <+48>: ret

3.2 LR寄存器

LR寄存器在gdb中可以直接查看x30的值,如下:

info register x30 x30 0x40071c 4196124

此时我们知道地址0x40071c,我们可以x解析值,这里gdb会帮我们提升为函数,如下

(gdb) x 0x40071c 0x40071c <main+116>: 0xb9401fe0

可以看到,这个地址是main函数+116的栈偏移地址。

此时我们设置断点如下:

(gdb) b *0x40071c Breakpoint 2 at 0x40071c: file ioctl.c, line 29.

此时我们运行,可以看到在断点停下了

Breakpoint 2, main () at ioctl.c:29 29 close(fd);

我们对应代码:

27 load_data(&d); 28 29 close(fd);

可以看到,正好在close上,也就是当前正好是load_data的返回。

通过这里,我们可以很清楚的知道,x30寄存器也就是lr寄存器,其作用是函数返回时的返回地址,这个地址是函数的运行地址。通过x30寄存器,我们可以定位函数位于上级函数的位置。

3.3 PC寄存器

PC寄存器虽然arm和aarch64在定义上有不同,但是在gdb中行为是一致的,我们可以直接查看pc的值,如下

(gdb) info register pc pc 0x400704 0x400704 <main+116>

这里可以看到,我们

当前的pc值是0x400704,此时我们可以反汇编如下:

(gdb) disassemble Dump of assembler code for function main: 0x0000000000400690 <+0>: stp x29, x30, [sp, #-32]! 0x0000000000400694 <+4>: mov x29, sp 0x0000000000400698 <+8>: mov w1, #0x2 // #2 0x000000000040069c <+12>: adrp x0, 0x400000 0x00000000004006a0 <+16>: add x0, x0, #0x828 0x00000000004006a4 <+20>: bl 0x400510 <open@plt> 0x00000000004006a8 <+24>: str w0, [sp, #28] 0x00000000004006ac <+28>: ldr w0, [sp, #28] 0x00000000004006b0 <+32>: cmp w0, #0x0 0x00000000004006b4 <+36>: b.ge 0x4006c0 <main+48> // b.tcont 0x00000000004006b8 <+40>: mov w0, #0x0 // #0 0x00000000004006bc <+44>: b 0x400710 <main+128> 0x00000000004006c0 <+48>: add x0, sp, #0x10 0x00000000004006c4 <+52>: mov x2, x0 0x00000000004006c8 <+56>: mov x1, #0x6162 // #24930 0x00000000004006cc <+60>: movk x1, #0x8008, lsl #16 0x00000000004006d0 <+64>: ldr w0, [sp, #28] 0x00000000004006d4 <+68>: bl 0x400570 <ioctl@plt> 0x00000000004006d8 <+72>: ldr w0, [sp, #16] 0x00000000004006dc <+76>: ldr w1, [sp, #20] 0x00000000004006e0 <+80>: ldr w2, [sp, #24] 0x00000000004006e4 <+84>: mov w3, w2 0x00000000004006e8 <+88>: mov w2, w1 0x00000000004006ec <+92>: mov w1, w0 0x00000000004006f0 <+96>: adrp x0, 0x400000 0x00000000004006f4 <+100>: add x0, x0, #0x838 0x00000000004006f8 <+104>: bl 0x400560 <printf@plt> 0x00000000004006fc <+108>: add x0, sp, #0x10 0x0000000000400700 <+112>: bl 0x400674 <load_data> => 0x0000000000400704 <+116>: ldr w0, [sp, #28] 0x0000000000400708 <+120>: bl 0x400530 <close@plt> 0x000000000040070c <+124>: mov w0, #0x0 // #0 0x0000000000400710 <+128>: ldp x29, x30, [sp], #32 0x0000000000400714 <+132>: ret

可以发现,这里有一个箭头,箭头地址就是pc的值。

我们可以知道,pc的值有三种修改方式

  • 代码运行时,按照4字节运行,这种方式会修改pc的值
  • 代码通过bl指令进行跳转,跳转后修改pc的值
  • adrp等加载指令可以修改pc的值(汇编代码未提供示例)

再加上pc寄存器有一个特性,指向的是下一条语句的运行,所以pc的值是即将运行的代码。

至此,我们了解了pc寄存器的值。

四、总结

至此,我们应该能够完全的理解aarch64的pc,sp和lr三个寄存器的完全解释了。

编辑
2025-01-22
记录知识
0

我们在讨论栈破坏的时候,通常是数组越界访问和库函数memset/memcpy等函数的错误覆盖,对于这样的错误,通常大家知道原因排查起来很方便,但是实际情况中,还有第三种栈破坏的问题,就是结构体不匹配的类型转换导致的栈破坏。本文讨论这种栈破坏问题。

一、发生情况

通常情况下,结构体的类型转换如果出现错误,编译的时候就会提示,例如将两个不同的结构体进行类型转换,如:

struct A a = (struct B)b;

这种错误编译器就告诉你不能这样转换。

但是如果是结构体指针,那么或许可以转换,例如

struct B *b; struct A* a = (struct A*)b;

我们发现可以将struct B的b指针强制类型转换成struct A,这种情况下编译器会提示warning

如果这种强制类型转换出现破坏了,我们查一下编译时的警告就一目了然了。

但是我们还有一种渠道,就是先将结构体指针struct B*转换成void *,然后将void*的变量转换成struct A*,如下:

struct B *b; void* t = b; struct A* a = (struct A*)t;

这种情况下,我们编译代码,将得不到任何警告或错误,因为对于C而言,这种转换本身就是有意义的。

但是如果我们代码直接这样写,那么当我们出现栈破坏的时候,我们可以通过审查代码的方式定位和解决问题。

如果我们的代码做过封装,封装的时候考虑了解耦,这种情况下,我们可以发现

struct B* 出现在服务层,我们需要在具体实现时构造struct B void* t=b 出现在接口层,为了解耦,我们需要将类型转换成通用的void struct A* 出现在客户端,我们在实现应用的时候,需要实例化一个对象,这个对象是struct A*的结构

就像这样

image.png 这种情况下,我们不方便追查问题,因为其真实的调用情况如下:

image.png 我们的app程序在调用libxxx.so时,根据的是libxxx_api.h,并不知道libxxx.so内部的实现,而app程序又会自行构造和类型转换结构体。

也就是说,如果libxxx.so的结构体和app的结构体声明存在不一致,那么可能出现问题。

那么有什么情况呢?这里以struct A和struct B为例,struct A在libxxx.so中实现,struct B在application中实现

image.png

  • 假设A的成员比B多,那么最后强制类型转换为B,我们可以发现是将实际结构体成员缩小了。并不会出现问题
  • 假设A的成员比B少,那么最后强制类型转换为B,我们可以发现B在访问变量的时候,就会错误的访问到栈区或堆区(取决于申请情况)
  • 假设A的结构体和B的结构体完全不一样,那么应用在访问数据的时候就能感知到业务出现了差异,问题很容易发现,所以不做讨论
  • 假设A的结构体和B的结构体完全一样,那么这属于正常行为,不会出现任何问题

根据上面所述的,我们存在问题的情况只有在2的时候,可能发生。这里为什么说可能发生,而不是一定发生呢,因为需要满足以下两点

  • A应该在栈区,栈区的数据才能破坏,如果在堆区,那么破坏的是堆区的数据
  • A应该破坏了自己或其他栈区结构体指针

如果A破坏了堆区,那么会出现堆区的异常,这里不做讨论

如果A只是占用了没分配的栈地址,那么程序不会异常

如果A只是修改了某个栈的局部变量,函数参数和临时数据,那么程序可能不会出现异常

所以这里的破坏,一定是破坏了某个栈区的结构体指针。

二、这种情况的发生场景

根据上面提到的,我们想要出现这种错误,那么我们肯定做了void*的转换,而实际调用so的过程中,我们其实没必要非做void*的类型转换,直接包含同一个h即可。那么什么场景下,这种转换尤为重要呢?

答案是这个数据来源于内核,因为内核的结构体和应用的结构体没有做好绑定。内核和应用又是隔离的。所以void*的转换尤为重要。

所以,综上可以知道。 如果我们通过内核拿一个结构体A,应用层的结构体B需要做强制类型转换,如果A和B结构体是完全一致的,那么不会存在问题,假设如下两种情况:

  1. 内核未更新,应用发生了更新,将结构体B的成员添加了,从而导致结构体B的成员比A多。
  2. 内核发生了更新,将结构体A的成员减少了,应用未发生更新,从而导致结构体B的成员比A多。

至此,我们将这种栈破坏讲述的比较清楚了。也可以发现,这种栈破坏在满足一定条件下,实际上是很容易出现的。

三、示例

3.1 直接复现问题

为了阐述这个问题,我们先编写了一个简单的c程序,该程序直接进行强制类型转换如下:

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> struct kernel{ int x; int y; int z; }; struct user{ int x; int y; int z; int o1; int o2; int o3; int o4; }; void test1(struct user* bug) { bug->x = 2; return; } void test(struct kernel k) { struct user* s = (struct user*)&k; s->o4 = 1; test1(s); return ; } int main(int argc, char *argv[]) { struct user arr = {1,2,3}; void* ss = &arr; struct kernel *b = (struct kernel*)ss; test(*b); return 0; }

我们将其编译:

gcc /root/stack_damage.c -o stack_damage

此时运行后。我们拿到一个段错误如下:

# ~/stack_damage 段错误 (核心已转储)

这里值得注意的是如下:

struct user arr = {1,2,3}; 声明了一个user的结构体 void* ss = &arr; 将其转换成void* struct kernel *b = (struct kernel*)ss; 将其强制类型转换成kernel void test(struct kernel k) 函数test会主动构造kernel结构体 struct user* s = (struct user*)&k; 此时强制类型转换给了s s->o4 = 1; 通过s破坏了结构体s自己 bug->x = 2; 这里访问自己的x时,出现了栈破坏的错误

这里还需要两个知识点如下:

  • 函数形参如果不超过16字节,那么不会使用x19来计算偏移,而是直接通过sp的偏移存储,也就是x0x1x2x3存放sp的偏移存储,也就是x0,x1,x2,x3存放sp开头的位置

9b13ee5fc9e85cc28c462df5ec1355c.jpg 上述查看B.4

  • 函数的第一个局部变量使用的是sp+sp+offset - 4,也就是第一个局部变量在栈区的最高位

如果忽略这两个要素,我们不会出现必现的栈破坏问题。

我们通过gdb来调试如下:

# gdb ./starck_damage GNU gdb (Ubuntu 9.1-0kylin1) 9.1 Copyright (C) 2020 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "aarch64-linux-gnu". Type "show configuration" for configuration details. For bug reporting instructions, please see: <http://www.gnu.org/software/gdb/bugs/>. Find the GDB manual and other documentation resources online at: <http://www.gnu.org/software/gdb/documentation/>. For help, type "help". Type "apropos word" to search for commands related to "word"... Reading symbols from ./starck_damage... (gdb)

此时运行如下:

(gdb) r Starting program: /root/stack_panic/starck_damage [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/aarch64-linux-gnu/libthread_db.so.1". Program received signal SIGSEGV, Segmentation fault. 0x0000000000400554 in test1 (bug=0x7f00000001) at /root/starck_damage.c:26 26 bug->x = 2;

然后我们得到了一个巨大的疑问

为什么bug->x =2 会出现段错误 此时我们应该现打一个断点test,然后重跑到断点如下:

(gdb) b test Breakpoint 1 at 0x40057c: test. (2 locations) (gdb) r The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /root/stack_panic/starck_damage [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/aarch64-linux-gnu/libthread_db.so.1". Breakpoint 1, test (k=...) at /root/starck_damage.c:32 32 struct user* s = (struct user*)&k;

然后定位到s->o4 = 1;之前,

(gdb) b *0x0000000000400588 Breakpoint 1 at 0x400588: file /root/starck_damage.c, line 33.

然后打印s的地址如下:

(gdb) p &s $1 = (struct user **) 0x7fffffeff8

然后计算s保存的值

(gdb) p s $2 = (struct user *) 0x7fffffefe0

我们得到地址0x7fffffefe0,然后查看其值

(gdb) x/3w 0x7fffffefe0 0x7fffffefe0: 0x00000001 0x00000002 0x00000003

此时我们将其定位到s->o4 = 1;运行之后,

(gdb) b *0x0000000000400590 Breakpoint 2 at 0x400590: file /root/starck_damage.c, line 34.

此时我们打印s的值,如下

(gdb) p s $4 = (struct user *) 0x7f00000001

可以发现s的值变成了0x7f00000001。此时我们访问

bug->x = 2;

自然出现段错误。

那么为什么会这样呢?

我们可以看到k的地址如下:

(gdb) p &k $7 = (struct kernel *) 0x7fffffefe0

然后s的地址是0x7fffffeff8

我们拿0x7fffffeff8-0x7fffffefe0=24

24/4+1=7

也就是说我们如果操作user的第7个int,那么就会破坏s的栈。

所以这也就是s->o4 = 1;的由来。

此问题根本原因是我们通过值传递结构体时,会通过寄存器+当前栈区空间来访问,此时如果强制类型转换,那么很可能访问到函数体内第一个栈区局部变量上,从而错误的改写了栈区内容。

至此,我们通过直接的示例阐述了破坏栈区的一种情况

3.2 真实的情况

根据上面说的,我们为了复现问题编写了c程序,但是真实的情况应该来自内核的结构体和应用的结构体不一致,所以我们先编写内核驱动如下:

struct data { int x; int y; int z; }; static long kylin_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { struct data user_data; user_data.x = 1; user_data.y = 2; user_data.z = 3; switch(cmd) { case RD_VALUE: if (copy_to_user((void __user *)arg, &user_data, sizeof(user_data))) { pr_err("Data Read : Err!\n"); } break; default: pr_info("Default\n"); break; } return 0; }

此时我们加载ko会在/dev/下出现kylin的字符设备

# ls /dev/kylin /dev/kylin

我们应用上先提供ioctl.c如下

#include <fcntl.h> #include <unistd.h> #include <sys/ioctl.h> #include "libs.h" #define RD_VALUE _IOR('a','b',int32_t*) void load_data(void* d) { struct kdata *b = (struct kdata*)d; test(*b); } int main() { int fd; struct kdata d; fd = open("/dev/kylin", O_RDWR); if(fd < 0) { return 0; } ioctl(fd, RD_VALUE, (struct kdata*) &d); printf("%d %d %d\n", d.x, d.y, d.z); load_data(&d); close(fd); }

值得注意的是这里的头文件如下:

root@kylin:~/stack_panic# cat libs.h kernel.h #include <stdio.h> #include <stdlib.h> #include <string.h> #include "kernel.h" void test(struct kdata k); #ifndef _KERNEL_STRUCT_H #define _KERNEL_STRUCT_H struct kdata { int x; int y; int z; }; #endif

我们知道应用需要调用test,所以实现libs.c如下

#include "libs.h" struct data { int x; int y; int z; int o1; int o2; int o3; int o4; }; void oops(struct data* bug) { bug->x = 2; return; } void test(struct kdata k) { struct data* s = (struct data*)&k; s->o4 = 1; oops(s); return ; }

编译如下:

gcc ioctl.c libs.c -g -o ioctl

此时运行时保存如下:

# ./ioctl 1 2 3 段错误 (核心已转储)

可以发现栈出现了破坏。

这里为了简化,我只是简单抽象了test的实现在libs.c中。但其意思符合上文描述的情况。

也就是说,应用如果调用了库,库的结构体和内核结构体不符合,那么就会出现栈破坏。

四、总结

本文通过示例的方式演示了一种特殊的栈破坏情况,这种情况在内核和应用之间非常容易出现(强制类型转换和小于16字节值传递的时候),也不容易排查。后续如果有其他情况出现这样的问题的时候,我们可以拿这种情况对号入座一下,这样就不用浪费更多的时间来排查栈破坏的问题了。