咱们有一个基本的面试题,有些同事可能存在一些误解的情况,本文基于这个题目介绍一下关于aarch64的情况
请说明计数寄存器(PC)和堆栈寄存器(SP)以及链接寄存器(LR)的作用 这个题目很清楚,需要阐述PC,SP和LR的作用,那不清晰的点在哪里呢
如果此问题是arm系列芯片,也就是32位,那么我们这里描述的是
R13,R14,R15
如下:
但是如果是在aarch64系列芯片,也就是64位,那么我们这里描述的是
X29,X30
如下:
值得注意的是,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寄存器可以在函数上可以直接通过偏移计算出来,或者通过adr等加载指令来计算偏移获取pc的值,而跳转可以通过bl来修改pc指针。
至此,我们从官方渠道了解了aarch64的关于pc寄存器的歧义点。
而实际上,在aarch64平台,我们上层使用过程中,仍是可以轻松的获取这些寄存器的值,和arm32相差无几,接下来我简单介绍一下在aarch64平台上这三个寄存器
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
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寄存器,我们可以定位函数位于上级函数的位置。
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的值有三种修改方式
再加上pc寄存器有一个特性,指向的是下一条语句的运行,所以pc的值是即将运行的代码。
至此,我们了解了pc寄存器的值。
至此,我们应该能够完全的理解aarch64的pc,sp和lr三个寄存器的完全解释了。
我们在讨论栈破坏的时候,通常是数组越界访问和库函数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*的结构
就像这样
这种情况下,我们不方便追查问题,因为其真实的调用情况如下:
我们的app程序在调用libxxx.so时,根据的是libxxx_api.h,并不知道libxxx.so内部的实现,而app程序又会自行构造和类型转换结构体。
也就是说,如果libxxx.so的结构体和app的结构体声明存在不一致,那么可能出现问题。
那么有什么情况呢?这里以struct A和struct B为例,struct A在libxxx.so中实现,struct B在application中实现
根据上面所述的,我们存在问题的情况只有在2的时候,可能发生。这里为什么说可能发生,而不是一定发生呢,因为需要满足以下两点
如果A破坏了堆区,那么会出现堆区的异常,这里不做讨论
如果A只是占用了没分配的栈地址,那么程序不会异常
如果A只是修改了某个栈的局部变量,函数参数和临时数据,那么程序可能不会出现异常
所以这里的破坏,一定是破坏了某个栈区的结构体指针。
根据上面提到的,我们想要出现这种错误,那么我们肯定做了void*
的转换,而实际调用so的过程中,我们其实没必要非做void*
的类型转换,直接包含同一个h即可。那么什么场景下,这种转换尤为重要呢?
答案是这个数据来源于内核,因为内核的结构体和应用的结构体没有做好绑定。内核和应用又是隔离的。所以void*
的转换尤为重要。
所以,综上可以知道。 如果我们通过内核拿一个结构体A,应用层的结构体B需要做强制类型转换,如果A和B结构体是完全一致的,那么不会存在问题,假设如下两种情况:
至此,我们将这种栈破坏讲述的比较清楚了。也可以发现,这种栈破坏在满足一定条件下,实际上是很容易出现的。
为了阐述这个问题,我们先编写了一个简单的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时,出现了栈破坏的错误
这里还需要两个知识点如下:
上述查看B.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
;的由来。
此问题根本原因是我们通过值传递结构体时,会通过寄存器+当前栈区空间来访问,此时如果强制类型转换,那么很可能访问到函数体内第一个栈区局部变量上,从而错误的改写了栈区内容。
至此,我们通过直接的示例阐述了破坏栈区的一种情况
根据上面说的,我们为了复现问题编写了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字节值传递的时候),也不容易排查。后续如果有其他情况出现这样的问题的时候,我们可以拿这种情况对号入座一下,这样就不用浪费更多的时间来排查栈破坏的问题了。
根据yocto版本构建(二)安装已有二进制和yocto版本构建(三)源地址dsc进行构建我们可以通过安装源的deb文件和通过dsc来执行一次构建操作,我们知道,通常情况下,代码都是通过git管理,这里还需要提供一个函数支持git方式的构建,如下:
对于gitlab2拉取源码的方式,我们需要指向到自己的SRC_URI后,根据deb.bbclass来进行代码构建,所以bbfile如下:
inherit kylin inherit deb SRC_URI = "git@gitlab2.kylin.com:shanghai-team/embedded/kylin-egf/ukui-menu.git;branch=egf/v101-tablet" S = "${WORKDIR}/ukui-menu" do_compile() { dpkg-buildpackage -uc -us : }
这里需要注意的是,我们没有使用yocto的本身git构建和打包deb的方式,我们使用了自己的命令,然后,我们需要在do_install的时候,进行dpkg -i的操作,这样sysroot才能正常安装。
为了使得调试更方便,这里提供了一些变量的说明,可以链接查看如下:
https://pages.openeuler.openatom.cn/embedded/docs/build/html/master/yocto/yocto_quick_start_manual/variables_and_tasks.html
主要解释如下:
MACHINE: 指定使用的硬件配置文件,通常在local.conf文件定义; DISTRO: 指定使用的发行版配置文件,通常在local.conf文件定义; PN: 软件包名,一般是根据文件名自动生成;除了一些交叉编译的包,如gcc-cross会在bb中重新定义; PV: 软件包版本; PR: 食谱的修订,默认为r0;当包管理器在已构建的镜像上动态安装包时,PR很重要,当前openEuler未启用; BPN: 软件包名,去除指定的前后缀(如-native、-cross等); BP: ${BPN}-${PV}; SRC_URI: 源码路径,可以为上游或者本地文件路径,上游源码需要使用校验值; LICENSE: 配方的源许可证列表,必须设置;如果设置为”CLOSED”则关闭; LIC_FILES_CHKSUM: 配方源代码中许可证文本的校验和,与LICENSE变量配合使用; PACKAGE_ARCH: 生成包的体系结构; TARGET_VENDOR: 指定目标供应商的名称,openEuler设置为”-openeuler”; TARGET_OS: 指定目标的操作系统; MULTIMACH_TARGET_SYS: 生成包的目标系统类型的唯一标识,默认为${PACKAGE_ARCH}${TARGET_VENDOR}-${TARGET_OS}; WORKDIR: 构建配方的工作目录的路径名,指向${TMPDIR}/work/${MULTIMACH_TARGET_SYS}/${PN}/${EXTENDPE}${PV}-${PR},EXTENDPE变量通常不被设置; S: 构建过程中源代码位置,默认为${WORKDIR}/${BPN}-${PV}; B: 构建过程中生成对象所在的目录,默认与S相同;一些类会将B设置为${WORKDIR}/build; D: 相当于 make install 后的目标目录,指向${WORKDIR}/image; PACKAGES: 表示配方创建的包列表; FILES_xxx: 放置在包中的文件和目录列表; PKGD: 要打包的文件的目录,指向${WORKDIR}/package; PKGDEST: 将文件拆分为单独的包后,指向要打包的文件的父目录,该目录是PACKAGES中指定的每个包的目录,指向${WORKDIR}/packages-split; DEPENDS: 列出配方的构建时依赖关系,配方在构建时需要其它配方的内容(例如头文件和共享库); RDEPENDS: 列出程序包的运行时依赖项,这些依赖项是必须安装的其他程序包,以便程序包正常运行; RECIPE_SYSROOT: 指向${WORKDIR}/recipe-sysroot; RECIPE_SYSROOT_NATIVE: 指向${WORKDIR}/recipe-sysroot-native; SYSROOT_DESTDIR: 指向${WORKDIR}/sysroot-destdir; SYSROOT_DIRS: 暂存到${SYSROOT_DESTDIR}的目录; STAGING_DIR_HOST: 组件运行所在的系统上的sysroot路径,默认为${RECIPE_SYSROOT}。 STAGING_DIR_NATIVE: 构建主机上运行的组件使用的sysroot的路径,默认为${RECIPE_SYSROOT_NATIVE}; STAGING_DIR_TARGET: 当构建在系统上执行的组件并为另一台机器生成代码(例如cross-canadian配方)时使用的sysroot路径; STAGING_KERNEL_DIR: 包含构建树外模块所需的内核头文件的目录(内核源码目录); STAGING_KERNEL_BUILDDIR: 指向包含内核构建工件的目录。需要访问内核构建工件的配方构建软件可以在内核构建后在STAGING_KERNEL_BUILDDIR变量指定的目录中查找这些工件; PACKAGE_CLASSES: 指定构建系统在打包数据时使用的包管理器(例如RPM、DEB或IPK),在local.conf文件设置; IMAGE_ROOTFS: 指定根文件系统在构建过程中的位置( do_rootfs 任务期间)。此变量不可配置,不要更改它; IMAGE_FEATURES: 指定要包含在镜像中的主要功能列表,这些功能大多数都映射到其他安装包; EXTRA_IMAGE_FEATURES: IMAGE_FEATURES的一部分; IMAGE_INSTALL: 指定要安装到镜像中的程序包; PACKAGE_EXCLUDE: :指定不应安装到image中的包; PACKAGE_INSTALL: 要安装到镜像中的程序包的列表,不要更改它,通常使用IMAGE_INSTALL变量间接进行修改; DEPLOY_DIR: 指向构建系统用于放置镜像、包、SDK和其他输出文件的常规区域,这些文件已准备好在构建系统之外使用。默认情况下,此目录位于指向${TMPDIR}/deploy。 DEPLOY_DIR_IMAGE: 指向构建系统用来放置准备部署到目标计算机上的镜像和其他相关输出文件的区域。该目录是特定于机器的默认情况下,此目录指向${DEPLOY_DIR}/images/${MACHINE}/; DEPLOYDIR: 当继承deploy类时,DEPLOYDIR指向已部署文件的临时工作区,默认指向${WORKDIR}/deploy-${PN},此目录内容会被拷贝到${DEPLOY_DIR_IMAGE}; CC: 用于运行C编译器的最小命令和参数; CFLAGS: 指定要传递给C编译器的标志; CXXFLAGS: 指定要传递给C++编译器的标志; CPPFLAGS: 指定要传递给C预处理器(即同时传递给C编译器和C++编译器)的标志; LDFLAGS: 指定要传递给链接器的标志; OVERRIDES: 以冒号分隔的当前应用的覆盖列表。覆盖是一种BitBake机制,允许在解析结束时选择性地覆盖变量; COMPATIBLE_MACHINE: 一种正则表达式,解析为一个或多个与配方兼容的目标机器。可以使用该变量来停止为配方不兼容的机器构建配方,停止这些构建对于内核特别有用。该变量还有助于提高解析速度,因为构建系统会跳过与当前机器不兼容的解析配方。
因为我们很多仓库都是通过git管理的,所以通过git地址来触发二进制编译的行为至关重要,根据上述描述,我们可以具备通过git链接地址进行二进制包的构建
根据yocto版本构建(三)源地址dsc进行构建我们可以通过输入一个内部源地址就可以拉取软件包安装,但是我们还需要提供一个功能,如果我们将文件放在recipes内的files下,我们需要手动将其解压和构建,这样可以不依赖源的dsc和文件
根据上面的描述,我们需要从files中获取文件,如下:
inherit kylin inherit deb SRC_URI = "file://ukui-menu_3.0.1-0720.1.dsc;md5=1b897ae127a2d8076a0b318daa720f91\ file://ukui-menu_3.0.1-0720.1.tar.xz;md5=e684d90a713f953179b1a06e8e3401c1 do_deb_prepend() { install -d ${S} install -m 0755 ${WORKDIR}/ukui-menu_3.0.1-0720.1.dsc${S} install -m 0644 ${WORKDIR}/ukui-menu_3.0.1-0720.1.tar.xz ${S} }
这样可以将ukui-menu的文件放到work目录下,然后我们执行dpkg命令即可正常构建
do_fetch
会使用SRC_URI变量定位源码文件;基于SRC_URI变量值中每个条目的前缀来确定使用哪个提取器来获取源文件, file://
开头为本地文件, http://
、git://
等为上游获取的源文件;
我们这里使用了file,这样将其拉到了sysroot内部,这样我们可以通过自己的函数来执行dpkg-buildpackage
。从而构建deb包
其形式类似于yocto版本构建(三)源地址dsc进行构建
至此,我们不仅可以通过远程源地址的dsc文件编译二进制包,也可以通过本地file链接来编译二进制包
根据yocto版本构建(二)安装已有二进制已经可以具备安装已有的二进制,这样一个完全仿照livebuild的方式(kybuilder)的yocto工程只需要花点时间就可以配置出来,但是我们不止如此,我们知道,麒麟v10操作系统软件包的开发是和deb开发方式一致的,通过dsc即可正常构建,如下是实现路径
为了支持debian包的构建,我们需要根据deb的安装方式设计一个bbclass用作bbfile的调用,我们知道,针对系统的软件开发,主要步骤如下:
wget https://xxxx.dsc dpkg -x xxxx.dsc dpkg-buildpackage -uc -us / dh_make
为了让yocto上也能使用dsc来通过源码的方式构建出一个deb包,我们能需要定义函数类似如下:
do_unpack_dsc() { dpkg-source -x ${DL_DIR}/${PN}_${PV}.dsc ${S} } do_compile_dsc() { dpkg-buildpackage -uc -us }
不仅如此,我们还需要在do_package中将其生成的deb文件,复制到recipes的work目录下。这样我们不需要使用yocto的本身构建方式来生成deb
对于bbfile,我们需要继承我们的类,然后提供一个宏定义,由我们的deb.bbclass获取代码开始构建,如下:
inherit kylin inherit deb DSC_URI = "https://dev.kylinos.cn/kylin-desktop/+archive/primary/+files/ukui-menu_3.0.1-0720.1.dsc;md5sum=1b897ae127a2d8076a0b318daa720f91"
为了让我们的bbfile支持调试,我们可以通过如下命令
bitback ukui-menu -c listtasks
这样可以列出我们的步骤,方便调试细节
如果我们需要进入devshell中,具体调试dpkg-buildpackage -uc -us
出现的错误,我们可以如下
bitback ukui-menu -c devshell
这样我们可以手动运行chroot进入此环境
至此,我们可以通过指定一个dsc文件,就可以触发yocto执行源码的编译动作