Coder Social home page Coder Social logo

blog's Introduction

blog's People

Contributors

carloscn avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

blog's Issues

Qt on Linux 使用deb打包发布

Distribute Qt APP on PPA of Ubuntu using the .deb package.

[Abstract]: 在Qt on Linux上开发程序完成后需要脱离本机所营造的开发库环境变量运行,则需要对二进制执行文件进行打包发布。在Ubuntu系统中通常使用deb格式文件对二进制安装包进行封装。Ubuntu系统提供了个人软件包(Personal Package Archives, PPA)在线安装deb功能。本文阐述在Linux系统中使用Qt编译生成的二进制文件收集*.so库文件过程、Ubuntu的deb打包过程,完成从Qt编译生成的二进制文件到发布到Ubuntu个人软件包文档全过程。

1. 收集支持库文件

使用Qt Creator开发的项目工程文件在release或debug模式下生成的二进制文件依赖于Qt Creator的环境变量,这是一个Qt运行库完整的环境变量。当二进制文件脱离该环境会提示加载依赖库失败。Linux系统ldd命令(ldd app)可以显示二进制文件依赖库。文献1提供了自动收集库文件的脚本。

对其进行改造命名为qt-lib-collect.sh,输入参数1目标文件,输入参数2库文件存储路径。

#!/bin/sh
# 可执行程序名
appname=$1
# 目标文件夹
dst=./$2
# 利用 ldd 提取依赖库的具体路径
liblist=$(ldd $appname | awk '{ if (match($3,"/")){ printf("%s "), $3 } }')
# 目标文件夹的检测
if [ ! -d $dst ];then
                mkdir $dst
fi
# 拷贝库文件和可执行程序到目标文件夹
cp $liblist $dst
cp $appname $dst

eg: ./qt-lib-collect.sh app out_dir

2. 使用deb打包

文献2提供整理了方法。使用deb打包,需要先创建符合deb格式规则的文件夹。在deb包中包括,DEBIAN目录和目标软件安装路径文件夹,例如,创建mydebpac文件夹,符合deb格式的最小文件夹结构为:

  • 文件夹DEBIAN,规定包含changlog contro copyright postinst postrm prerm文件,这些文件对deb进行描述,需要按照deb定义的字段格式填写。
  • 文件夹/opt:视为用户使用dpkg -i安装该deb时,该包的安装文件会解压到用户的/opt路径下,同理若设定为/usr,则会解压到/usr下。
  • 文件postinst:脚本文件。在文件目录拷贝后执行。相应地,preinst为文件安装前执行脚本。
  • 文件prerm:脚本文件。在文件目录卸载掉前执行。相应地,postrm为文件卸载后的执行脚本。
  • 文件contro:deb文件包描述,其内容有包名称、版本、依赖库、包介绍等。

所有的脚本文件,都需要预先给定执行权限chmod +x ....

2.1 contro文件

Package: tinySerial
Version: v1.1
Section: free
Priority: optional
Depends: libssl.0.0.so, libstdc++2.10-glibc2.2
Suggests: Openssl
Architecture: amd64
Installed-Size: 61952
Maintainer: [email protected]
Provides: mysoftware
Description: tinySerial is a opensource GUI serial port debug software on Linux.

2.2 postinst文件

在postinst内可以写一些处理快捷方式、配置环境变量的操作。

# !/bin/sh
cp /opt/tinyserial/tinyserial.desktop /usr/share/applications
cp /opt/tinyserial/tinyserial /usr/bin

笔者会做两个处理:

  1. 将*.desktop文件复制到app中心,路径/usr/share/applications
  2. 将二进制可执行文件复制到/usr/bin

2.3 postrm文件

与postinst回滚执行的文件。

# !/bin/sh
rm /usr/share/applications/tinyserial.desktop
rm /usr/bin/tinyserial

2.4 生成deb包

dpkg -b tinyserial tinyserial-v1.1.deb

附录I 常用deb操作命令2

安装deb包:

dpkg -i mydeb.deb

卸载deb包:

dpkg -r mysoftware

查看deb包是否安装:

dpkg -s mysoftware

查看deb包文件内容:

dpkg -c mydeb.deb

查看当前目录某个deb包的信息:

dpkg --info mydeb.deb

dpkg -X mydeb.deb mydeb

解压deb包中DEBIAN目录下的文件(至少包含control文件)

dpkg -e mydeb.deb mydeb

Footnotes

  1. 追火车. 【Qt依赖库】Linux 环境下 Qt 可执行程序依赖库打包脚本.

  2. 新月时刻. dpkg制作deb包. 2

09_ARMv8_内嵌汇编(内联汇编)Inline assembly

09_ARMv8_内嵌汇编(内联汇编)Inline assembly

内联汇编并非ARCH64专门的使用方法,而是GNU编译器通用的做法。目的有二,其一,对于时间敏感的函数使用内联汇编减少执行开销;其二,C语言无法访问架构级的特殊指令,比如内存屏障功能。

1. 基本用法

1.1 基础内联汇编格式

Define:asm ("asm instruction")

【示例】:

asm(icicalluis) 调用一条高速缓存维护指令。

1.2 扩展内联汇编代码

  • Define:asm qualifier-asm (AssemblerInstruction)

  • Instruction:

    • 格式:指令部分:输出部分:输入部分:损坏部分
    • 编译器不会解析,按照字符串处理
  • 限定词:volatile,inline

    • volatile:无特殊情况下的限定词
    • inline:asm的语句视为代码最小可能性

【示例1】:

static inline unsigned long array_index_mask_nospec(unsigned long idx, unsigned long sz)
{
	unsigned long mask;
  asm volatile(
  "			cmp			%1, %2\n"								// 指令部第一行
  "     sbc     %0, xzr, xzr\n"         // 指令部第二行,使用\n换行
  : "=r" (mask)                         // 输出部分,指定只写属性的变量mask(%0)
  : "r" (idx), "Ir" (sz)                // 输入部分,执行只读部分的变量idx(%1), sz(%2)
  : "cc");                              // 损坏部分,跳出汇编
  csdb();
  return mask;
}

【示例2】:

static inline unsigned long arch_local_irq_save(void)
{
  unsigned long flags;
  asm volatile(
  "			mrs		%0, daif\n"
  "     msr   daifset, #2\n"
  : "=r" (flag)
  :
  : "memory"
  );
}

1.2.1 修饰符1

C Describe
= Means that this operand is written to by this instruction: the previous value is discarded and replaced by new data. (只能写)
+ Means that this operand is both read and written by the instruction. (读写)
& Means (in a particular alternative) that this operand is an earlyclobber operand, which is written before the instruction is finished using the input operands. Therefore, this operand may not lie in a register that is read by the instruction or as part of any memory address.(输入参数的指令执行完成之后才能写入)

1.2.2 约束符2

C Describe
p An operand that is a valid memory address is allowed. This is for “load address” and “push address” instructions. (内存地址)
m A memory operand is allowed, with any kind of address that the machine supports in general. Note that the letter used for the general memory constraint can be re-defined by a back end using the TARGET_MEM_CONSTRAINT macro.(内存变量)
o A memory operand is allowed, but only if the address is offsettable. This means that adding a small integer (actually, the width in bytes of the operand, as determined by its machine mode) may be added to the address and the result is also a valid memory address.(内存地址,基地址寻址)
r A register operand is allowed provided that it is in a general register. 通用寄存器
i An immediate integer operand (one with constant value) is allowed. This includes symbolic constants whose values will be known only at assembly time or later. 立即数
V A memory operand that is not offsettable. In other words, anything that would fit the ‘m’ constraint but not the ‘o’ constraint. 内存变量,不允许偏移的内存操作数
n An immediate integer operand with a known numeric value is allowed. Many systems cannot support assembly-time constants for operands less than a word wide. Constraints for these operands should use ‘n’ rather than ‘i’.离结束

除了通用的约束符之外,还有AARCH64特有的约束符3

C Describe
k The stack pointer register (SP)
w Floating point register, Advanced SIMD vector register or SVE vector register
x Like w, but restricted to registers 0 to 15 inclusive.
y Like w, but restricted to registers 0 to 7 inclusive.
Upl One of the low eight SVE predicate registers (P0 to P7)
Upa Any of the SVE predicate registers (P0 to P15)
I Integer constant that is valid as an immediate operand in an ADD instruction
J Integer constant that is valid as an immediate operand in a SUB instruction (once negated)
K Integer constant that can be used with a 32-bit logical instruction
L Integer constant that can be used with a 32-bit logical instruction
M Integer constant that is valid as an immediate operand in a 32-bit MOV pseudo instruction. The MOV may be assembled to one of several different machine instructions depending on the value
N Integer constant that is valid as an immediate operand in a 64-bit MOV pseudo instruction
S An absolute symbolic address or a label reference
Y Floating point constant zero
Z Integer constant zero
Ush The high part (bits 12 and upwards) of the pc-relative address of a symbol within 4GB of the instruction
Q A memory address which uses a single base register with no offset
Ump A memory address suitable for a load/store pair instruction in SI, DI, SF and DF modes

【示例3】:分析atomic_add函数内嵌汇编

void my_atomic_add(unsigned long val, void *p)
{
  unsigned long tmp;
  int result;
  asm volatile (
  "		1: 	ldxr %0, [%2]\n"
  "       add %0, %0, %3\n"
  "       stxr %w1, %0, [%2]\n"
  "       cbnz %w1, 1b\m"
  : "+r" (tmp), "+r" (result), "+Q" (*(unsigned long *)p)
  : "r" (val)
  : "cc", "memory"
  );
}

内联汇编还支持助记符:

int add(int i, int j) {
  int ret = 0;
  asm volatile (
  "	add %w[result], %w[input_i], %w[input_j]\n"
  : [result] "=r" (res)
  : [input_i] "r" (i), [input_j] "r" (j)
  :
  );
}

实验:实现memcpy,使用内联汇编的方式。

void test_memcpy(void)
{
    unsigned long src_addr = 0x80000, dest_addr = 0x200000;
    unsigned long sz = 32;

    asm volatile (
    "       mov x6, %x[_src_addr]\n"
    "       mov x7, %x[_dest_addr]\n"
    "       add x8, x6, %x[_sz]\n"
    "   1:  ldr x9, [x6], #8\n"
    "       str x9, [x7], #8\n"
    "       cmp x6, x8\n"
    "       b.cc 1b\n"
    :
    : [_src_addr] "r" (src_addr), [_dest_addr] "r" (dest_addr), [_sz] "r" (sz)
    : "cc", "memory"
    );
}

以下需要注意:

  • GDB不能单步调试内联汇编。建议使用纯汇编单独编写调试之后移植到C语言内部。
  • 内联汇编的参数属性是易错点。
  • 输出部和输入部的修饰符不能用错,否则程序会跑飞。比如参数寄存器x0,指定在输入部,汇编内部对参数做了修改比如用了add指令,那么就跑飞了。

再做一个例子memset

void test_memset(void)
{
    unsigned long addr = 0x80000;
    unsigned long sz = 16;
    unsigned long i = 0;

    asm volatile (
    "       mov x4, #0\n"
    "   1:  stp %x[_count], %x[_count], [%x[_addr]], #16\n"
    "       add %x[_sz], %x[_sz], #16\n"
    "       cmp %x[_sz], %x[_addr]\n"
    "       bne 1b\n"
    : [_addr] "+r" (addr), [_sz] "+r" (sz), [_count] "+r" (i)
    :
    : "memory"
    );
}

2. 宏函数

内联汇编也可以和C语言的宏联系到一起,使用##字符串拼接的方式,在内联汇编里面使用""双引号来引用宏参数。

#define MY_OPS(ops, asm_ops)                                        \
static inline void my_asm_##ops(unsigned long mask, void *p) {      \
    unsigned long tmp;                                              \
    asm volatile (                                                  \
        "       ldr %1, [%0]\n"                                     \
        "       "#asm_ops" %1, %1, %2\n"                            \
        "       str %1, [%0]\n"                                     \
        : "+r" (p), "+r" (tmp)                                      \
        : "r" (mask)                                                \
        : "memory"                                                  \
    );                                                              \
}

MY_OPS(or, orr)
MY_OPS(and, and)
MY_OPS(andnot, bic)

最后宏展开为几个函数:

  • my_asm_or
  • my_asm_and
  • my_asm_andnot

这三个函数只是在内联汇编里面 "#asm_ops"位置不同。

Ref

Footnotes

  1. GCC - 6.47.3.3 Constraint Modifier Characters

  2. GCC - 6.47.3.1 Simple Constraints

  3. GCC - 6.47.3.4 Constraints for Particular Machines

05_ELF文件_动态链接

05_ELF文件_动态链接

Q1:为什么要使用动态链接?

  • 第一,静态链接会占用更多的空间,如果a.elf和b.elf在操作系统里面都引用了libc的函数,那么使用静态链接a.elf和b.elf都会复制libc副本到内存中,增大了内存和磁盘空间。在内存**享一个模块:节省内存,还可减少物理页面的换入换出,也可增加CPU缓存的命中率,因为不同进程间的数据和指令访问都集中在了同一个共享模块上。
  • 第二,从程序发布角度,如果a.elf和b.elf文件共用的c.so文件出现了bug,那么c.so重新release,a.elf和b.elf也需要重新release,如果这里有个十分highlevel程序调用了多级库,每一级的修改都会要重新release,这无疑是痛苦的。
  • 第三,可扩展性,动态链接可以实现可扩展性。开发者可以暴露出接口让用户来实现,实现了插件的功能。

Q2:动态链接有什么弊端?

  • DLL hell现象,旧的动态链接和新的动态链接不兼容。
  • 执行速度没有静态链接快。(这里涉及查表、重定位等多个过程)动态链接与静态链接相比,性能损失大约在5%以下1,但这点性能损失用来换取程序在空间上的节省和程序构建和升级时的灵活性,是相当值得的。

Q3:Linux和windows操作系统动态链接有什么区别?

  • Linux的ELF动态链接文件被称为动态共享对象(DSO, Dynamic shared objects),通常以so结尾;Windows中的动态链接被称为动态链接库(Dynamical linking library)。

1 gcc动态链接

1.1 生成和使用动态链接

这里举个例子如何生成动态链接库

// libsum.c
#include "libsum.h"

int lib_sum(int a, int b) {
    return a + b;
}
// main.c 
#include "libsum.h"
#include <stdio.h>
int main(void)
{
    int a = 0xf, b = 0x7;
    sleep(-1);
    return lib_sum(a, b);
}

aarch64-none-elf-gcc -fPIC -shared -o libsum.so libsum.c -I .

  • -shared: 表示产生共享对象
  • -fPIC: 表示地址无关代码

使用lib:

aarch64-none-elf-gcc main.c libsum.so -o main.elf --specs=nosys.specs

注意--specs=nosys.specs是arm的baremental环境里面特别要求的,与动态库无关。

运行应用程序:(需要指定LD_LIBRARY_PATH的路径)

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:./ && ./main.elf &

1.2 研究动态链接的空间分布

使用cat /proc/581540/maps查看进程的的相关的内存映射,这里面主要涉及了3个so文件:

image-20220406194037155

  • libc:里面引用了sleep之类的函数
  • libsum:自己实现的动态库
  • ld-2.31:这个是linux动态链接器,动态链接用于管理动态库的映射。

再来看下libsum.so的装载属性readelf -l libsum.so

image-20220406194510435

Note,动态链接模块的装载地址是从0x0开始的,而真正的地址是0x7fd1e0cb1000,共享对象的最终装载地址在编译的时候不是确定的。

2 装载时重定位和地址无关代码

2.1 装载时重定位

操作系统需要根据内存空闲情况,动态分配一块物理内存给程序,所以程序被装载的地址是不确定的。系统在装载程序的时候需要对程序的指令和数据中绝对地址的引用进行重定位,基地址变换之后加上偏移量就能拿到重定位后的地址。静态链接完成的是链接时重定位,现在这种情况被叫做装载时重定位

gcc -shared a.c -o a.so此时使用装载时重定位,我理解这也是作为so文件共享对象必然要有的属性,动态链接库必然根据每个依赖他的程序不同,装载时进行重定位。

2.2 地址无关代码

还记得我们在ARCH64架构上面提出的LDR指令和ADRP指令里面有个对于VMA和LMA加载地址时候崩溃的问题(1.4 ADRP和LDR的陷阱)。里面设计了ADRP和LDR绝对地址和相对于PC地址的偏移的事情。实际上在动态链接里面完全可以看见这样设计的缘由。如果一个进程使用了动态链接,装载时重定位只是相当于把动态链接库的地址重定位了而已,实际上还是一种静态重定位。而如果多个进程同时使用动态链接,就没有动态链接宣称节约内存的优势,因此,必须要实现:多个进程之间指令部分共用一份,数据自己独享的方式,这里就引出了地址无关代码的技术。

地址无关代码技术我们在ARCH64指令集里面也可以看到,很多指令都是相对寻址(相对于PC)。我们按照程序员自我修养里面的方式,来研究一下aarch64上面有关4种情况:

  • 模块内部的函数调用和跳转
  • 模块内部的数据访问
  • 模块外部的函数调用和跳转
  • 模块外部的数据访问
static int a;
extern int b;
extern void lib_sum();

void bar()
{
    a = 1;
    b = 2;
}

void main()
{
    bar();
    lib_sum(a, b);
}

这里面bar(),a是内部的函数和数据;lib_sum,b是外部的。我们编译一下$ aarch64-none-elf-gcc -c exa.c libsum.so -o exa.o

使用objdump查看反汇编:aarch64-none-elf-objdump -S -h exa.o

$ aarch64-none-elf-objdump -S -h exa.o

0000000000400494 <lib_sum@plt>:
  400494:	90000090 	adrp	x16, 410000 <__FRAME_END__+0xf3e0>
  400498:	f9470211 	ldr	x17, [x16, #3584]
  40049c:	91380210 	add	x16, x16, #0xe00
  4004a0:	d61f0220 	br	x17
  
0000000000400668 <bar>:
  400668:	b0000080 	adrp	x0, 411000 <impure_data+0x1e0>
  40066c:	91168000 	add	x0, x0, #0x5a0
  400670:	52800021 	mov	w1, #0x1                   	// #1
  400674:	b9000001 	str	w1, [x0]
  400678:	b0000080 	adrp	x0, 411000 <impure_data+0x1e0>
  40067c:	9115a000 	add	x0, x0, #0x568
  400680:	52800041 	mov	w1, #0x2                   	// #2
  400684:	b9000001 	str	w1, [x0]
  400688:	d503201f 	nop
  40068c:	d65f03c0 	ret

0000000000400690 <main>:
  400690:	a9bf7bfd 	stp	x29, x30, [sp, #-16]!
  400694:	910003fd 	mov	x29, sp
  400698:	97fffff4 	bl	400668 <bar>
  40069c:	b0000080 	adrp	x0, 411000 <impure_data+0x1e0>
  4006a0:	91168000 	add	x0, x0, #0x5a0
  4006a4:	b9400002 	ldr	w2, [x0]
  4006a8:	b0000080 	adrp	x0, 411000 <impure_data+0x1e0>
  4006ac:	9115a000 	add	x0, x0, #0x568
  4006b0:	b9400000 	ldr	w0, [x0]
  4006b4:	2a0003e1 	mov	w1, w0
  4006b8:	2a0203e0 	mov	w0, w2
  4006bc:	97ffff76 	bl	400494 <lib_sum@plt>
  4006c0:	d503201f 	nop
  4006c4:	a8c17bfd 	ldp	x29, x30, [sp], #16
  4006c8:	d65f03c0 	ret
  4006cc:	d503201f 	nop

2.2.1 GOT表

模块间的数据访问依托的是.GOT表(全局偏移表Global Offset Table),当代码引用外部的变量的时候,需要通过这个表来查找到偏移量。通过objdump -h exa.so
image-20220408145917918

再通过objdump - R exa.so
image-20220408150006700

b的位置在0x10be0的位置,.got在0x10bd8,在其偏移量0x8的位置。

PIC的代码不包含任何代码段重定位表。

2.2.2 PLT表

在调用外部库的时候<lib_sum@plt>有个符号plt,这个就是延迟绑定技术。这个目的是,可以提高动态链接库的执行速度。在ELF文件中,比如有很多分支里面调用的函数,很低的概率被用到,那么这个函数会被延迟绑定,就是这个函数第一次被用到的时候才会被绑定(查找、重定位)。

我们在ELF段结构里面看到GOT表,就分为两种:

  • GOT表
  • GOT.plt表

3 动态链接相关结构

3.1 动态链接器

我们在查看linux进程的/proc/xxx/maps的时候,里面会有ld.so,这个是由系统提供的一个动态链接器(Dynamic Linker)。启动一个包含动态链接的程序的步骤:

  • 操作系统加载动态链接器
  • 动态链接器初始化,根据环境变量对ELF执行动态链接
  • 动态链接器把控制权交给可执行文件的入口函数

.interp段指定ld.so的路径。通常Linux系统下,都是/lib/ld-linux.so.2这个文件。

.dynamic段保存了动态链接器的基本信息,地址、哈希表地址、等等。可以用readelf -d xx.so命令查看。ldd命令还可以查看elf依赖了哪些库。

Ref

Footnotes

  1. 静态链接和动态链接优缺点

02_ELF文件结构_浅析内部文件结构

ELF文件结构

1. 文件结构

1.1. Scope

通用的ELF文件,我们可以分为四大类:

  • Header: 描述基本属性的
  • Sections:各个段,包括.text .data .bss等
  • Section header tables:ELF中所有段的段名、段长度、文件偏移、读写权限等
  • Helper tables:辅助结构,字符串表、符号表。
----------------------------------------
       ELF Header(描述基本属性)               
----------------------------------------
       .text (段1)
----------------------------------------
       .data (段2)
----------------------------------------
       .bss  (段3)
----------------------------------------
       ...other 
          sections...
----------------------------------------
       Section header tables (段表)
----------------------------------------
       String Tables   (字符串表)
       Symbol Tables   (符号表)
----------------------------------------

1.2. Header

aarch64-none-linux-gnu-gcc main.c -o main

readelf -h main

readelf -h main
ELF Header:
  // e_ident members
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 // ELF 魔数
  Class:                             ELF64                 // 有ELF32 / ELF64
  Data:                              2's complement, little endian 
  Version:                           1 (current)  // always 1
  OS/ABI:                            UNIX - System V  
  ABI Version:                       0
  
  // e_type members
  Type:                              EXEC (Executable file)
  // e_machine members
  Machine:                           AArch64
  // e_version members
  Version:                           0x1
  // e_entry members: 规定ELF程序的入口的虚拟地址,操作系统在加载完程序后从这个地址开始执行进程指令
  Entry point address:               0x4004c0
  // e_phoff members:ELF链接视图和执行视图相关
  Start of program headers:          64 (bytes into file)
  // e_shoff member:段表在文件中的偏移,从13521字节开始
  Start of section headers:          13520 (bytes into file)
  // e_word
  Flags:                             0x0
  // e_ehsize: ELF文件头的大小
  Size of this header:               64 (bytes)
  // e_phentsize: ELF链接视图和执行视图相关
  Size of program headers:           56 (bytes)
  // e_phnum: ELF链接视图和执行视图相关
  Number of program headers:         9
  // e_shentsize: 段表描述符的大小 一般为sizeof(Elf32_Shdr)
  Size of section headers:           64 (bytes)
  // e_shnum: 段表描述符的数量。
  Number of section headers:         38
  // e_shstrndx: 段表字符串表所在的段在段表中的下标
  Section header string table index: 37

ELF的类型可以参考:elf(5) - Linux manual page (man7.org)

1.3. Section Header Table(段表)

段表的作用是,在ELF中记录每个段的基本属性(段名、段的长度、在文件中的偏移、读写权限及段的其他属性)。编译器和链接器还有装载器都需要依靠段表来定位和访问各个段的属性。在elf文件头中e_shoff 决定段表的存储位置。

readelf -S main

➜  work-temp readelf -S main
There are 38 section headers, starting at offset 0x34d0:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000000400238  00000238
       000000000000001b  0000000000000000   A       0     0     1
  [ 2] .note.ABI-tag     NOTE             0000000000400254  00000254
       0000000000000020  0000000000000000   A       0     0     4
  [ 3] .hash             HASH             0000000000400278  00000278
       0000000000000028  0000000000000004   A       5     0     8
  [ 4] .gnu.hash         GNU_HASH         00000000004002a0  000002a0
       000000000000001c  0000000000000000   A       5     0     8
  [ 5] .dynsym           DYNSYM           00000000004002c0  000002c0
       0000000000000078  0000000000000018   A       6     1     8
  [ 6] .dynstr           STRTAB           0000000000400338  00000338
       0000000000000044  0000000000000000   A       0     0     1
  [ 7] .gnu.version      VERSYM           000000000040037c  0000037c
       000000000000000a  0000000000000002   A       5     0     2
  [ 8] .gnu.version_r    VERNEED          0000000000400388  00000388
       0000000000000020  0000000000000000   A       6     1     8
  [ 9] .rela.dyn         RELA             00000000004003a8  000003a8
       0000000000000018  0000000000000018   A       5     0     8
  [10] .rela.plt         RELA             00000000004003c0  000003c0
       0000000000000060  0000000000000018  AI       5    23     8
  [11] .init             PROGBITS         0000000000400420  00000420
       0000000000000018  0000000000000000  AX       0     0     4
  [12] .plt              PROGBITS         0000000000400440  00000440
       0000000000000060  0000000000000000  AX       0     0     16
  [13] .text             PROGBITS         00000000004004c0  000004c0
       0000000000000214  0000000000000000  AX       0     0     64
  [14] CARLOS_FUNC       PROGBITS         00000000004006d4  000006d4
       0000000000000030  0000000000000000  AX       0     0     4
  [15] .fini             PROGBITS         0000000000400704  00000704
       0000000000000014  0000000000000000  AX       0     0     4
  [16] .rodata           PROGBITS         0000000000400718  00000718
       000000000000002f  0000000000000000   A       0     0     8
  [17] .eh_frame_hdr     PROGBITS         0000000000400748  00000748
       000000000000005c  0000000000000000   A       0     0     4
  [18] .eh_frame         PROGBITS         00000000004007a8  000007a8
       000000000000012c  0000000000000000   A       0     0     8
  [19] .init_array       INIT_ARRAY       0000000000410de8  00000de8
       0000000000000008  0000000000000008  WA       0     0     8
  [20] .fini_array       FINI_ARRAY       0000000000410df0  00000df0
       0000000000000008  0000000000000008  WA       0     0     8
  [21] .dynamic          DYNAMIC          0000000000410df8  00000df8
       00000000000001e0  0000000000000010  WA       6     0     8
  [22] .got              PROGBITS         0000000000410fd8  00000fd8
       0000000000000010  0000000000000008  WA       0     0     8
  [23] .got.plt          PROGBITS         0000000000410fe8  00000fe8
       0000000000000038  0000000000000008  WA       0     0     8
  [24] .data             PROGBITS         0000000000411020  00001020
       0000000000000018  0000000000000000  WA       0     0     8
  [25] CARLOS_DATA       PROGBITS         0000000000411038  00001038
       0000000000000004  0000000000000000  WA       0     0     4
  [26] .bss              NOBITS           000000000041103c  0000103c
       000000000000000c  0000000000000000  WA       0     0     4
  [27] .comment          PROGBITS         0000000000000000  0000103c
       000000000000005d  0000000000000001  MS       0     0     1
  [28] .debug_aranges    PROGBITS         0000000000000000  000010a0
       0000000000000130  0000000000000000           0     0     16
  [29] .debug_info       PROGBITS         0000000000000000  000011d0
       0000000000000715  0000000000000000           0     0     1
  [30] .debug_abbrev     PROGBITS         0000000000000000  000018e5
       00000000000002a1  0000000000000000           0     0     1
  [31] .debug_line       PROGBITS         0000000000000000  00001b86
       000000000000037d  0000000000000000           0     0     1
  [32] .debug_str        PROGBITS         0000000000000000  00001f03
       00000000000004ea  0000000000000001  MS       0     0     1
  [33] .debug_loc        PROGBITS         0000000000000000  000023ed
       0000000000000182  0000000000000000           0     0     1
  [34] .debug_ranges     PROGBITS         0000000000000000  00002570
       0000000000000090  0000000000000000           0     0     16
  [35] .symtab           SYMTAB           0000000000000000  00002600
       0000000000000b10  0000000000000018          36    90     8
  [36] .strtab           STRTAB           0000000000000000  00003110
       0000000000000258  0000000000000000           0     0     1
  [37] .shstrtab         STRTAB           0000000000000000  00003368
       0000000000000161  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  p (processor specific)

以上表为下列结构体的数组

typedef struct
{
  Elf64_Word	sh_name;		/* Section name (string tbl index) */
  Elf64_Word	sh_type;		/* Section type */
  Elf64_Xword	sh_flags;		/* Section flags */
  Elf64_Addr	sh_addr;		/* Section virtual addr at execution 如果该段可以被加载,显示为进程空间的虚拟地址,否则为0 */
  Elf64_Off		sh_offset;		/* Section file offset 这段对.bbs没有意义 */
  Elf64_Xword	sh_size;		/* Section size in bytes */
  Elf64_Word	sh_link;		/* Link to another section */
  Elf64_Word	sh_info;		/* Additional section information */
  Elf64_Xword	sh_addralign;	/* Section alignment 有的段有对齐要求,0/1表示没有对其要求 */
  Elf64_Xword	sh_entsize;		/* Entry size if section holds table */
} Elf64_Shdr;

重定位表 Relocation Table

在其他段里面引用了一些段的地址,这些地址单独存成一个重定位表,例如printf("hello world"), 里面的字符串就要被重定位到一个单独的区域。

字符串表 String Table

段名、变量名长度不固定,单独放在一个连续的区域里面,使用偏移来索引字符串。.strtab,.shstrtab字符串表, 在header里面 e_shstrndx: 段表字符串表所在的段在段表中的下标 表示

附录 I: 原始C文件

// main.c
#include <stdio.h>

int a = 84;
int b;
const int g = 0xAA;
void func(int i)
{
    printf("helloworld!%d\n", i);
}

__attribute((section("CARLOS_DATA"))) int name = 4;
__attribute((section("CARLOS_FUNC"))) int func2 (void){
    int m = 9, n = 10;
    int q;
    q = m+n;
    return q;
}

int main(void)
{
    static int var_1 = 85;
    static int var_2;
    int c = 6;
    int d;
    func(var_1 + var_2 + c + d);
    return c;
}

11_ARMv8_异常处理(二)- Legacy 中断处理

11_ARMv8_异常处理(二)- Legacy 中断处理

异常处理还没有完成,在异常处理(一)里面算是把ARMV8a上面的一些比较基本的异常相关的知识罗列出来了,但是还没有具体的分析。在ARM的世界,中断是异常的一种。这一节的目标是利用Linux内核研究:

  • ARMv8架构下的中断处理过程?
  • Linux内核在ARMv8a的架构下中断是如何处理?
  • ARMv8a中断一些局限性,Linux内核的机制如何弥补?
  • Cortex-A72多核多CPU的中断协调问题?
  • Cortex-M33(armv8-m)与arm-v7a中断处理与armv8a的不同?
  • freeRTOS事件处理机制在Cortex-M33上的处理?

ARMv8的中断有两种模式可以配置,一种是传统的中断处理Legacy模式,一种是使用GIC中断管理器模式。我们这次目标是Legacy模式,下一节异常处理(三)会学习GIC中断管理。


1 Overview interrupt

image-20220416132633869

外部设备assert一个中断线,中断控制器会产生一个IRQ使CPU进入到异常处理流程,保存一些PC->ELR(子函数返回地址),保存PSTATE状态信息,还会切入EL1异常等级(栈指针),接着就是CPU和Kernel共同完成的中断向量表,根据中断向量表的地址进入到内核的异常handler,执行内核中断上下文。

1.1 Interrupt controller

1.1.1 RTL design

我并非研究ARM的RTL设计的人员,但是从RTL里面能给我们提供一些本质的信息。我们这里用ZYNQ设计的双核Cortex-A9为例1,看看整个中断控制器如何工作的。

image-20220416134100235

中断控制器的RTL设计如图所示,我在图片上标注了中断控制器的输入输出,输入一共是两个,分别是图中的source 1和source 2,一个是外部的通过外设引脚输入进来的中断源,一个是内部的CPU产生的中断源,通过或的关系合并成FIQ和IRQ两个中断输出,中断的输出被接入到CPU0和CPU1的接口。我们在CPU上能观测到的就是异常的产生。

  • CPU内部中断:Timer,AWDT
  • CPU外部中断:SPI

我们可以在PSTATE寄存器中控制IRQ和FIQ的开关。

1.1.2 Legacy interrupt controller

参考一些做CortexA72的二级产商的技术手册,NXP的imx82和德州仪器TDA4VM3,并没有设计相应的legacy中断,我以树莓派的Cortex A72(x4)为例4,了解一下legacy interrupt怎么设计的,我们在异常处理(三)中研究一下不同厂商如何将GIC集成到自己的ARM处理器上的。

image-20220416140153009

树莓派是这样做的,将中断线分为几类,ARM coreN(arm核心组中断),ARM_LOCAL(只有CPU能访问的中断), ARMC(CPU和GPU共享的中断),videocore(GPU的中断)和ETH_PCIe(网络PCIE中断)。

image-20220416145105813

中断比较多,传统的中断寄存器就是使用串联的方法,待定寄存器三个FIQn/IRQn_PENDING1 + FIQn/IRQn_PENDING0,还有一个PENDING2,串联起来,相当于把ARMC和外设类的中断打包,共用一个位,最后组装成ARM_LOCAL FIQ/IRQ_SOURCEn寄存器。所以我们使用这些寄存器来控制和读取中断状态。

在树莓派的手册里面举了个例子,一个UART4的FIQ中断发送到ARM core3上的处理时序:

  • 进入到FIQ的handler
  • 读FIQ_SOURCE3
  • 判断FIQ_SOURCES3[8]为1, 就去读FIQ3_PENDING2
  • 判断FIQ3_PENDING2[25]为1,就去读FIQ3_PENDING1
  • 判定FIQ3_PENDING1[25]为1,就去读PACTL_CS[20:16]
  • 判断PACTRL_CS[17]为1,就去读UART4_MIS断定什么造成的中断。

这部分应该是中断上半段处理的内容。

1.2 CPU work

中断也是一种异常和异常处理(一)的处理方式一样,摘录于异常处理(一):

CPU自动做的事情:

  • S1: PSTATE保存到SPSR_ELx
  • S2: 返回地址PC保存到ELR_ELx
  • S3: PSTATE寄存器里面的DAIF域都设定为1(等同于关闭调试异常、SError,IRQ FIQ中断)
  • S4: 更新ESR_ELx寄存器(包含了同步异常发生的原因)
  • S5: SP执行SP_ELx
  • S6: 切换到对应的EL,接着跳转到异常向量表执行

操作系统需要做的事情:

  • 备份上面的寄存器到栈。

  • 识别异常发生的类型

  • 跳转到合适的异常向量表(包含异常跳转函数)

  • 处理异常

  • 操作系统执行eret指令

当中断发生的时候有两个现场需要保护:

  • CPU需要保护中断现场,CPU在EL0异常等级,将中断现场都保存在EL1的栈里面。
  • Kernel保护中断现场,发生在中断上半段。

这两个中断现场有个比较明显的分界线,就是是否跳转异常向量表。CPU的中断现场在跳转异常向量表之前,此时还没有进入到EL1,但是备份到EL1;Kernel的保护中断现场上半段,此时已经在CPU的EL1,栈已经切到了EL1的栈空间上面,保护程序逻辑的现场。

我们看一下CPU保护中断现场的工作,kernel保护中断现场放在1.4.1 top half来说。CPU发生异常的时候会自动把SP/PC/PSTATE这些数据备份到寄存器里面,但是这里有个问题,如果我们对这个数据不加以备份,当异常嵌套异常的时候,寄存器的值就会被冲走,我们只能恢复一级的异常,因此还要备份异常到相应的栈里面,当多级嵌套的异常从根节点一路路返回的时候,从栈中拿出寄存器的数据进行返回。这个好比QQ幻想里面的飞空艇仙子,龙城->长乐村->桃源村->天都,我们手里的信息只能回溯到上一站,等我们返回桃源村的时候,打开飞空艇仙子的对话框,就不知道去哪里了。因此我们每飞一个地方之后,需要在各个城市的飞空艇仙子的复印一个副本,告诉他我来自哪里,到天都之后我就知道回到桃源村,到桃源村的时候我就知道来自长乐村,最后我就能返回龙城。这就相当于中断现场的保护,在每一次中断过来之后,备份SP/PC/PSTATE到栈空间。

image-20220416160526722

在entry.S中kernel_entry就是CPU保护中断现场的工作,当然处理保护中断现场,还有很多关于内存的指令,这里面先不做过多的关注。

image-20220416161416901

REGS STACK Content
SPSR_EL1 pt.pstate PSTATE
ELR_EL1 pt.sp SP
LR pt.regs[30] LR
X29 pt.regs[29] X29
X28-X0 pt.regs[28..0] X28-X0

1.4 kernel:exception handler

这部分我们在Linux Kernel专题里面来讨论,在ARM64处理器这块就暂时不讨论了。

2 Example

对于Legacy中断模式,我们启动系统的定时器,在Cortex-A72处理器的树莓派4b上面实现一个定时器,需要自己完成寄存器的配置,异常向量表的跳转,寄存器的备份工作。

2.1 Timer Pre-knowledge

2.1.2 regs

Cortex-A72一共是有4个定时器5

定时器 ELx 安全模式 虚拟 中断源
PS定时器 EL1 安全模式 物理定时器 CNT_PS_IRQ
PNS定时器 EL1 非安全模式 物理定时器 CNT_PNS_IRQ
HP定时器 EL2 x 虚拟环境下的物理定时器 CNT_HP_IRQ
V定时器 EL1 x 虚拟定时器 CNT_V_IRQ

相关寄存器也在这里6

ame Type Reset Width Description
CNTKCTL_EL1 RW -a 32-bit Timer Control register (EL1)
CNTFRQ_EL0 RW b UNK 32-bit Timer Counter Frequency register
CNTPCT_EL0 RO UNK 64-bit Physical Timer Count register
CNTVCT_EL0 RO UNK 64-bit Virtual Timer Count register
CNTP_TVAL_EL0 RW UNK 32-bit Physical Timer TimerValue (EL0)
CNTP_CTL_EL0 RW -c 32-bit Physical Timer Control register (EL0)
CNTP_CVAL_EL0 RW UNK 64-bit Physical Timer CompareValue register (EL0)
CNTV_TVAL_EL0 RW UNK 32-bit Virtual Timer TimerValue register
CNTV_CTL_EL0 RW -c 32-bit Virtual Timer Control register
CNTV_CVAL_EL0 RW UNK 64-bit Virtual Timer CompareValue register
CNTVOFF_EL2 RW UNK 64-bit Virtual Timer Offset register
CNTHCTL_EL2 RW -d 32-bit Timer Control register (EL2)
CNTHP_TVAL_EL2 RW UNK 32-bit Physical Timer TimerValue register (EL2)
CNTHP_CTL_EL2 RW -c 32-bit Physical Timer Control register (EL2)
CNTHP_CVAL_EL2 RW UNK 64-bit Physical Timer CompareValue register (EL2)
CNTPS_TVAL_EL1 RW UNK 32-bit Physical Timer TimerValue register (EL2)
CNTPS_CTL_EL1 RW -c 32-bit Physical Secure Timer Control register (EL1)
CNTPS_CVAL_EL1 RW UNK 64-bit Physical Secure Timer CompareValue register (EL1)

4个通用定时器总中断设置是在ARM_LOCAL的中断组的寄存器中配置。到此,其实我读到这里就有个疑问了,根据手册,CPU的4个通用定时器是在ARM_Core的中断组的,ARM_LOCAL里面只有一个local timer并不是这4个ARM的通用定时器,为什么要在ARM_LOCAL中断组寄存器里面配置。后面看了legacy中断的路由信息可以注意到:

image-20220417123141473

虽然几个定时器被分配到了ARMCore组,但是实际上是在ARM_LOCAL里面的寄存器处理的,这个设计真的是有点令人很迷惑。

2.1.2 config

定时器需要配置,根据常识都需要:

  • 配置时间,计数多久?
  • 中断子开关,中断总开关。
  • 开始计时
ARM: CNTP_CTRL_EL0

我们选定的是CNT定时器,也就是EL1里面的PS定时器。在手册里面,CNTP_CTL_EL0来配置,这里还有个奇怪的事情,就是PS定时器是EL1里面的,但是配置寄存器是在CNTP_CTL_EL0内配置的(我搜了一下手册,是没有CNTP_CTL_EL1的,可能配置都是EL0寄存器里面完成的)

CNTP_CTL_EL0, counter_timer physical timer control register.

image-20220417132038101

bits function
0: ENABLE Enables the timer. 0 关闭,1打开
1: IMASK interrupt 掩码位, 0不会被iMASK bit掩码;1会被 iMASK bit掩码
2: ISTATUS 定时器的状态,0定时器中断状态不满足,1定时器中断状态满足
timer_ps0_enable:
    ldr x4, =TIMER_CNTRL0_REG_ADDR
    mov x5, #2
    str x5, [x4]
    ret
ARM: CNTP_TVAL_EL0

Timervalue的初始值,这个值会递减到0的时候会触发中断,还需要在handler里面重新赋值。

image-20220417134737306

#define ARM_LOCAL_REG_BASE_ADDR (0xFF800000)
#define TIMER_CNTRL0_REG_ADDR   (ARM_LOCAL_REG_BASE_ADDR + 0x40)

timer_ps0_enable:
    ldr x4, =TIMER_CNTRL0_REG_ADDR
    mov x5, #2
    str x5, [x4]
    ret
CortexA72: TIMER_CNTRLx

image-20220417135133905

image-20220417135233516

树莓派一共三个这样的寄存器,这个寄存器用于软件决定从ARM core接收一个FIQ的中断请求。我们关注的应该是BIT0,是cortex-A72处理器内核的PS定时器。

timer_ps0_set_value:
    msr cntp_tval_el0, x0
    ret
ARM: PSTATE

配置PSTATE上面的DAIF使能总的中断开关。

arch_enable_daif:
		msr	daifclr, #2
    ret

arch_disable_daif:
    msr	daifset, #2
    ret

2.1.3 process

// IRQ_SOURCE0_REG_ADDR
void irq_handle(void)
{
	unsigned int irq = 0;
	unsigned int regs = IRQ_SOURCE0_REG_ADDR;

	irq = readl(IRQ_SOURCE0_REG_ADDR);
	switch (irq) {
		case (CNT_PNS_IRQ):
		handle_timer_irq(100);
		break;

		default:
		printk("Unkown IRQ 0x%x\n", irq);
		break;
	}
}

void handle_timer_irq(unsigned int val)
{
	timer_ps0_set_value(val);
	printk("Core0 timer interrupt recved\n\r");
}

3 Reference

Footnotes

  1. FPGA Technical Tutorials - Zynq System-on-Chip Design Overview Interrupts

  2. i.MX 8QuadMax Applications Processor Reference Manual - 3.1.2 Interrupt Interface

  3. DRA829/TDA4VM Technical Reference Manual (Rev. C) - 9.1 Interrupt Architecture

  4. BCM2711 ARM Peripherals

  5. ARM Cortex-A72 MPCore Processor Technical Reference Manual r0p3 - Generic Timer functional description

  6. ARM Cortex-A72 MPCore Processor Technical Reference Manual r0p3 - AArch64 Generic Timer register summary

04_ELF文件_加载进程虚拟地址空间

04_进程虚拟地址空间

这个稍稍微微有点操作系统里面的东西了,《程序员的自我修养》一本书里面,是站在可执行程序里面看的操作系统,比如说程序是分页管理的方式,大概说明了一下分页什么原理,而从《操作系统原理》一本书里面是站在操作系统的角度去看可执行程序,就会具体的讲操作系统是如何分页的,比如hash页表,页偏移具体的公式。两部分可以说是完全不一样的视角,本文意图就是希望结合两个视角整理出一份自己理解的笔记。

  • 进程虚拟地址
  • 装载方式
  • 进程虚拟空间分布
  • ELF角度看堆和栈

1. 进程虚拟地址(VMA)

早期程序在ROM中直接加载到RAM里面执行,这种方法就十分简陋;RAM是一个十分珍贵的资源,这种方式当然不能被接受,后来人们提出了Overlay覆盖装入的方法,但这样的方法需要程序员自己对互不相关没有调用的函数进行管理,需要根据依赖关系组织成树状结构,对于开发者十分不友好;现代操作系统使用也映射(paging)的方法,按页完成数据和指令在ROM和RAM中的换入和换出(SWAP);现在操作系统还引入了MMU,让这种paging变得更为复杂。

1.1 ELF -> PVS -> PMS

最终程序的执行从编译出的ELF逻辑空间,会被映射到PVS(进程虚拟空间),最后被MMU映射到PMS(物理内存空间)。我们在编译一个文件的时候在ELF文件中readelf可以看到VMA的地址,VMA的地址是虚拟内存区域(Virtual Memory Area)。关于ELF->PVS,我们这里关注点在于ELF->PVS加载的过程,对于PVS->PMS属于MMU的知识范畴,这里暂时不涉及,相关TOPICS也会在ARM架构和Linux内核里面着重展开。

1.1.1 image load 单个段

进程在开始初始化的时候读取可执行文件头,并且建立虚拟空间和可执行文件的映射关系。(ELF -> PVS)这个过程我们可以叫做映像文件加载(image loading)。

image-20220403130558621

1.1.2 页错误Page Fault

我们在开头的时候说,线代操作系统采用paging的方法,既然是节约RAM的方法,必然是从存储在ROM中的ELF部分页映射到PVS上面。这里其实有几个很简单的concern:

  • 什么时候映射,映射规则如何?
  • 如果进程执行的时候发现下一条指令或者需要的数据没有被映射怎么办?

文献1提到swap技术中的两种方式,一种是是标准交换,一种是移动系统的交换。文献2提供了多个页面置换的算法供操作系统选择。而当进程执行的时候发现指令数据没有被映射,那么就会发生页错误(Page Fault),此时就会激发缺页中断,CPU强制抢占进程执行,在缺页中断的下半段,在不同的系统中就会采用不同的页面置换算法,当中断返回之后进程继续执行,在PVS内就有对应的PMS了。所以,页错误也是评价一个页面置换算法的好坏的指标,为了追求更好的性能,尽可能的减少页错误的发生。(文献1给了一组关于系统切换上下文的数据,100MB的进程,传输速度为50MB/s,100MB进程换入和换出的时间需要4s。)

页错误的一种场景3

image-20220403140728712

进程会选择性映射DP1中的指令到PVS的VP0,VP1和VP5,如果他们的指令分别对应1+1,1+2,1+3,进程按照顺序执行他们,这种映射关系被存储在两个结构体上面,左侧一个结构体存储ELF->PVS的,右侧结构体存储PVS->PMS的。当进程需要1+4指令的时候,在结构体ELF->PVS可以查找到关系,但在PVS->PMS结构体查找不到对应关系,此时出发page fault中断,CPU开始运行页面置换算法,中断执行完毕,回到进程中,继续查找PVS->PVM的映射关系,此时如果建立了如图红色的映射关系,那么1+4这条指令可以被查询到接着执行。这就是整个page fault的处理流程。

1.1.3 swap技术

这里和生活中一些我们在应用观察的现象做一个引申。文献4提到苹果的ISO内存管理一些比较初级的原理,我们在使用ISO的时候不会担心RAM被吃光,甚至库克不建议清理ISO后台。我相信一部分原因也是因为没有采用swap技术,当然也进程管理及低功耗管理肯定有着很大的作用,从文献4摘录:

iOS中,内存分为两种,一种为Clean memory,另一种为Dirty memory
Clean memorypage可以换出,既磁盘中有其对应内容,系统可以在内存紧张时将Clean memorypage换出,当再次访问时,可以重新从磁盘中读取,我们使用的图片、mapped filesFramework的数据段常量以及代码段等,这些都是Clean memory
Dirty memory是无法换出的,我们所有的堆上的分配等都是属于Dirty memory,所以我们一定要尽可能的减少Dirty memory的使用。

而Linux使用这项技术,我们在安装UBUNTU分盘的时候,常常需要预留2GB的SWAP空间,实际上就是在这里应用。那么应用SWAP的切换进程上下文,还有双缓冲都会吃掉一些性能。

1.2 链接视图和执行视图

1.2.1 section和segment区别

如果我们自己定义了很多段,甚至一个变量占了一个段,那么VMA在映射的时候就很肉痛了,因为ELF文件被映射的时候是以页为单位的(意味着映射长度是系统页单位的数倍,Linux系统页可能是4KB/8KB/16KB,利用getpagesize()或者命令行getconf PAGESIZE)。如果定义一个变量独占一个段,那么可能在VMA上面占据4KB的长度。在VMA角度不会关心你自己定义的这些段5,VMA角度只关心这些段的可读可写可执行的属性还有逻辑地址的值。因此,在装载过程中:对于权限相同的段进行合并之后再映射。这里有个约定俗成的叫法,我们把没有之前的段叫做section,把合并之后的段叫做Segment5,当我们写gcc程序的时候出现Segment fault这种错误的时候,就可以知道是访问内存权限出错了。

1.2.2 查看section及segment

查看section就是我们老方法了,readelf -S xxx.elf,把debug一些section去掉了。这个叫做链接视图(Linking View)

There are 36 section headers, starting at offset 0x2f50:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000000400200  00000200
       000000000000001b  0000000000000000   A       0     0     1
  [ 2] .note.ABI-tag     NOTE             000000000040021c  0000021c
       0000000000000020  0000000000000000   A       0     0     4
  [ 3] .note.gnu.bu[...] NOTE             000000000040023c  0000023c
       0000000000000024  0000000000000000   A       0     0     4
  [ 4] .hash             HASH             0000000000400260  00000260
       0000000000000024  0000000000000004   A       5     0     8
  [ 5] .dynsym           DYNSYM           0000000000400288  00000288
       0000000000000060  0000000000000018   A       6     1     8
  [ 6] .dynstr           STRTAB           00000000004002e8  000002e8
       000000000000003d  0000000000000000   A       0     0     1
  [ 7] .gnu.version      VERSYM           0000000000400326  00000326
       0000000000000008  0000000000000002   A       5     0     2
  [ 8] .gnu.version_r    VERNEED          0000000000400330  00000330
       0000000000000020  0000000000000000   A       6     1     8
  [ 9] .rela.dyn         RELA             0000000000400350  00000350
       0000000000000018  0000000000000018   A       5     0     8
  [10] .rela.plt         RELA             0000000000400368  00000368
       0000000000000048  0000000000000018  AI       5    21     8
  [11] .init             PROGBITS         00000000004003b0  000003b0
       0000000000000014  0000000000000000  AX       0     0     4
  [12] .plt              PROGBITS         00000000004003d0  000003d0
       0000000000000050  0000000000000010  AX       0     0     16
  [13] .text             PROGBITS         0000000000400420  00000420
       0000000000000294  0000000000000000  AX       0     0     8
  [14] .fini             PROGBITS         00000000004006b4  000006b4
       0000000000000010  0000000000000000  AX       0     0     4
  [15] .rodata           PROGBITS         00000000004006c8  000006c8
       0000000000000034  0000000000000000   A       0     0     8
  [16] .eh_frame         PROGBITS         00000000004006fc  000006fc
       0000000000000004  0000000000000000   A       0     0     4
  [17] .init_array       INIT_ARRAY       0000000000410df8  00000df8
       0000000000000008  0000000000000008  WA       0     0     8
  [18] .fini_array       FINI_ARRAY       0000000000410e00  00000e00
       0000000000000008  0000000000000008  WA       0     0     8
  [19] .dynamic          DYNAMIC          0000000000410e08  00000e08
       00000000000001d0  0000000000000010  WA       6     0     8
  [20] .got              PROGBITS         0000000000410fd8  00000fd8
       0000000000000010  0000000000000008  WA       0     0     8
  [21] .got.plt          PROGBITS         0000000000410fe8  00000fe8
       0000000000000030  0000000000000008  WA       0     0     8
  [22] .data             PROGBITS         0000000000411018  00001018
       0000000000000034  0000000000000000  WA       0     0     8
  [23] .bss              NOBITS           000000000041104c  0000104c
       0000000000000014  0000000000000000  WA       0     0     4
  [24] .comment          PROGBITS         0000000000000000  0000104c
       0000000000000024  0000000000000001  MS       0     0     1
  [34] .strtab           STRTAB           0000000000000000  000029c8
       0000000000000431  0000000000000000           0     0     1
  [35] .shstrtab         STRTAB           0000000000000000  00002df9
       0000000000000157  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  D (mbind), p (processor specific)

查看segment的方法:aarch64-none-elf-readelf -l ab.elf

Elf file type is EXEC (Executable file)
Entry point 0x400420
There are 8 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
                 0x00000000000001c0 0x00000000000001c0  R E    0x8
  INTERP         0x0000000000000200 0x0000000000400200 0x0000000000400200
                 0x000000000000001b 0x000000000000001b  R      0x1
      [Requesting program interpreter: /lib/ld-linux-aarch64.so.1]
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x0000000000000700 0x0000000000000700  R E    0x10000
  LOAD           0x0000000000000df8 0x0000000000410df8 0x0000000000410df8
                 0x0000000000000254 0x0000000000000268  RW     0x10000
  DYNAMIC        0x0000000000000e08 0x0000000000410e08 0x0000000000410e08
                 0x00000000000001d0 0x00000000000001d0  RW     0x8
  NOTE           0x000000000000021c 0x000000000040021c 0x000000000040021c
                 0x0000000000000044 0x0000000000000044  R      0x4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     0x10
  GNU_RELRO      0x0000000000000df8 0x0000000000410df8 0x0000000000410df8
                 0x0000000000000208 0x0000000000000208  R      0x1

 Section to Segment mapping:
  Segment Sections...
   00
   01     .interp
   02     .interp .note.ABI-tag .note.gnu.build-id .hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_frame
   03     .init_array .fini_array .dynamic .got .got.plt .data .bss
   04     .dynamic
   05     .note.ABI-tag .note.gnu.build-id
   06
   07     .init_array .fini_array .dynamic .got

segment中的段,叫做执行视图(Execution View)

  • LOAD:属于我们应该关心的地址2个,第一个地址是VMA0,属于可读可执行的区域;第二个地址是VMA1,属于可读可写的区域。
  • DYNAMIC:动态链接的时候再说
  • NOTE, GNU_STACK, GNU_RELRO:起到辅助作用,不说了。

我们可以从视图的角度定义section和segment,section是链接视图里面的概念,而segment是执行视图里面的概念。

2. ELF角度看堆和栈

我们程序内部肯定是用到堆(Heap)和栈(Stack)了,这部分在elf上面怎么表述的,尤其是使用malloc从堆分配空间这是一个动态的过程?malloc肯定是从PVS上面分配的VMA,那么如何表示,难道编译器可以预测malloc的行为?

2.1 查看maps

我们在IMX6ULL的嵌入式Linux上面查看test_msg_1.elf进程的虚拟空间分布,cat /proc/pid/maps如图所示:

image-20220403145309995

图中第一列就是VMA的范围,第二列权限,第三列偏移,第四列主设备和次设备号,第五列映像文件的节点号,最后是文件路径。这个程序里面有很多动态链接库的地址,还有一些用[]标注起来的地址,他们被称为匿名虚拟内存区域(Anoymous Virtual Memory Area)

  • [heap] : 136KB大小

  • [stack] : 132KB大小

  • [vdso] : 该地址已经位于内核空间,用于和内核进行一些通信,暂时不在本文展开。

VMA 说明
代码VMA RW,有映像文件
数据VMA RWX,有映像文件
堆VMA RWX,无影响文件,匿名,可向上扩展
栈VMA RW,无影响文件,匿名,可向下扩展

2.2 栈和堆groth方向

image-20220403152021482

这里多说一个stack和heap有趣的知识,stack和heap的增长方向,stack的增长方向是向下增长的(高地址->低地址),而heap的增长方向是(低地址到高地址)6。这个原因就是,STACK和HEAP是动态区域,很难划分出界限,所以让两个区域朝着一个方向滚动,可以充分利用各自的空间。

2.3 非头部映射

还有一个需要注意的是,Linux装载ELF的时候并直接映射,比如上图.RW区域头地址并非对应DATA区域的头地址。linux内核里面有个“hack”的方法,简言之就是上个区域没用完的段会被插入一些其他的段__libcfreeres_ptrs,这种做法在Linux内核的elf_map(), laod_elf_interp()中。

2.4 堆最大申请及ASLR

malloc申请空间的具体数值是一个不确定的数值,因为受到操作系统、程序本身大小、用到的动态库共享库大小、程序栈数量、大小等。甚至每次运行结果都不同,操作系统为了安全性还使用了随机地址空间技术(Address Space Layout Randomization, ASLR)7,使得进程的堆空间变得更小。

#include <stdio.h>
#include <stdlib.h>

unsigned int max = 0;

int main()
{
    unsigned int blocksize[] = {1024 * 1024, 1024, 1};
    int i, count;
    for (i = 0; i < 3; i++)
    {
        for (count = 1; ; count++)
        {
            void* block = malloc(max + blocksize[i] * count);
            if (block)  //malloc ok
            {
                max += blocksize[i] * count;
                free(block);
            }
            else
            {
                break;
            }
        }
    }
    printf("max malloc size = %uB\n", max);
    return 0;
}

3. 相邻页合并

我们装载以段为基本单位划分,使得内存的利用率提升了,但是我们总是要一直追求内存的利用率不断提升的,比我们继续分析,假如有三个段,大小分别为127,9899,1988,那么他们分别需要1,3,1个页,一共需要5个页,但是每个页都存在页内碎片,尤其是第一个段,仅仅127的大小,却要给其分配一整个页,三个段大小一共是12014字节,但是却需要20480字节的空间,空间使用率只有58.6%,所以我们继续思考如何提高内存利用率。

一般大部分unix系统采用的都是相邻页合并的方法,注意 : 我们采用相邻页合并,改变的是物理内存的分页方式,虚拟内存中我们依然是按照正常方式分配页,比如上例中,我们采用相邻页合并,会使得分配的物理页数目减少,但是虚拟内存还是按照正常分配方式分配5个,只不过会存在虚拟内存中两个页映射到同一个物理内存页上,比如,让第一段的唯一一个页和相邻段(该段有3个页)的相邻页合并成为一个页,如下图

image-20220403160317500

左图是没有分段的情况,SEG0和SEG2单独使用一个页,SEG1由于比较庞大使用了三个页,因此用了5个页。而右边的图中对ELF进行页大小分割,在PMS上面,page1包含了SEG2和部分SEG1,page2被SEG1的部分独占,page3包含了SEG0,ELF头和SEG1的部分,最后依靠MMU把PMS共享的部分展开到PVS上面。相邻页合并使页的使用率提高。

4. 进程加载ELF的过程

  • 检查ELF格式的有效性,包含magic,程序头表中段(segment)的数量。
  • 寻找动态链接.interp段,设定动态链接的路径。
  • 根据ELF表头的描述,对ELF文件进行映射,code,data,和只读区域。
  • 初始化ELF进程环境。(参照动态链接)
  • 将系统调用的返回地址修改成ELF的入口地址。(load_elf_binary -> do_execve -> sys_execve),此时系统调用从内核态返回到用户态的时候,直接跳转到ELF的地址了。
  • ELF装载完成。

5. Canary和ASLR

我相信计算机发展的今天,很多设计都是为了弥补一些场景的漏洞,而这些漏洞作为年轻一代的我们并没有参与过,不过读读相关论文和博客资料能从中挖掘一点乐趣,也能了解一下计算机专业的同学到底在研究什么。1988年的时候莫里斯李用unix操作系统fingerd软件中缓冲溢出安全漏洞写出了莫里斯蠕虫,基于缓冲区(堆栈)溢出的原理。基于缓冲区的攻击包括栈溢出、堆溢出、整形溢出、格式化字符串攻击、双重free8,基于植入性的攻击包括代码植入攻击还有return-into-libc攻击。

怎么攻击?我们可以看到没有开启ASLR的动态库是固定值,黑客如果想要攻击系统可以修改这个固定值加载的动态链接:栈溢出或者return-into-libc来实现攻击。攻击者可以通过缓冲区溢出改写返回地址为一个自己实现的库函数地址,并将库函数执行的参数也重新写入栈,这样函数调用时获取的是攻击者的设定好的参数,并且结束之后返回到函数而不是main,具体操作过程可以参考9,里面通过栈溢出操作获取root权限(编译器关闭–fno-stack-protector)。

5.1 SSP编译保护Canary

SSP(Stack Smashing Protect)摘了原文10

Canary保护机制的原理,是在一个函数入口处从gs(32位)或fs(64位)段内获取一个随机值,一般存到eax - 0x4(32位)或rax -0x8(64位)的位置。如果攻击者利用栈溢出修改到了这个值,导致该值与存入的值不一致,__stack_chk_fail函数将抛出异常并退出程序。Canary最高字节一般是\x00,防止由于其他漏洞产生的Canary泄露

需要注意的是:canary一般最高位是\x00,64位程序的canary大小是8个字节,32位的是4个字节,canary的位置不一定就是与ebp存储的位置相邻,具体得看程序的汇编操作

首先,canary被检测到修改,函数不会经过正常的流程结束栈帧并继续执行接下来的代码,而是跳转到call __stack_chk_fail处,然后对于我们来说,执行完这个函数,程序退出,屏幕上留下一行*** stack smashing detected ***:[XXX] terminated。这里的[XXX]是程序的名字。很显然,这行字不可能凭空产生,肯定是__stack_chk_fail打印出来的。而且,程序的名字一定是个来自外部的变量(毕竟ELF格式里面可没有保存程序名)。既然是个来自外部的变量,就有修改的余地。

程序正常的走完了流程,到函数执行完的时候,程序会把canary的值取出来,和之前放在栈上的canary进行比较,如果因为栈溢出什么的原因覆盖到了canary而导致canary发生了变化则直接终止程序。

在GCC里面开启canary保护11

-fstack-protector 启用保护,不过只为局部变量中含有数组的函数插入保护
-fstack-protector-all 启用保护,为所有函数插入保护
-fstack-protector-strong
-fstack-protector-explicit 只对有明确stack_protect attribute的函数开启保护
-fno-stack-protector 禁用保护.

# 使用checksec查看保护
adef@ubuntu:~$ checksec microwave
[*] '/microwave'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    FORTIFY:  Enabled

但是Canary也并不安全,一方面Canary仅仅是对于stack的保护,对heap没有保护作用12,另一方面canary也可能被fork()爆破10。Canary被爆破之后程序会立刻技术,重新进入程序之后Canary的值也会变化。但是同一个进程的canary值都是一致的,当祖先进程不断fork的时候,劫持__stack_chk_fail函数就可以将canary爆破。

5.2 ASLR地址空间随机化

Canary这种方法只局限于对栈的保护。空间地址随机化是通过对操作系统内核或者C库进行修改。使得进程加载到内存地址是随机化,从而降低攻击成功的概率。在return-to-libc或者.plt/.got覆盖场景下,攻击者必须知道指定地址。地址空间随机化之后,指定地址难以确定,达到抗攻击的可能。地址空间随机化包含四个层面12

  • 栈的随机化:/usr/src/sys/kem/kem_exec.c里面。
  • 堆的随机化:malloc初始化上面实现
  • 动态库映射随机化:内核mmap函数实现
  • 可执行映像随机化:gcc需要支持-fpie选项

这里就不具体解析了,参考文献12

在Linux Userspace中对ASLR的使用7

地址空间随机化
ASLR(Address Space Layout Randomization)
查看当前系统的ASLR配置情况

image-20220403154048815

cat /proc/sys/kernel/randomize_va_space
sysctl -a --pattern randomize

配置选项

0 关闭
1 半随机 共享库 栈 mmap()以及VDSO将被随机化
2 全随机 还有heap

开启后基址每次加载都会发生变化

echo 0 > /proc/sys/kernel/randomize_va_space
sysctl -w kernel.randomize_va_space=0

image-20220403154100094

使用ldd命令可以观察程序所依赖动态加载模块的地址空间,当ASLR开启时,地址就会发生变化

Ref

Footnotes

  1. 操作系统概念(原书第9版)- 第三部分 内存管理/8.2 交换 2

  2. 页面置换算法

  3. 页错误 Page Fault /缺页异常 详解

  4. iOS内存管理之Swapped Memory 2

  5. 程序员的自我修养 : 链接、装载与库 2

  6. 进程中堆栈向下增长的原因

  7. ASLR(地址空间随机化) 2

  8. 地址空间随机化技术研究

  9. 20145236《网络对抗》进阶实验——Return-to-libc攻击

  10. Stack Canary 2

  11. stack canary绕过思路

  12. FreeBSD的地址空间随机化 2 3

基于OMAPL138的Linux字符驱动_GPIO驱动AD9833(二)之cdev与read、write

基于OMAPL138的Linux字符驱动_GPIO驱动AD9833(二)之cdev与read、write

0. 导语

在上一篇博客里面,基于OMAPL138的字符驱动_GPIO驱动AD9833(一)之ioctl 中使用#include <linux/miscdevice.h>中的miscdevice机制,在呢篇博客中使用宋宝华的Linux驱动设备中提供的cdev机制完成注册,

根据参考文献[1]中所说:

misc设备其实也是字符设备,主不过misc设备驱动在字符设备的基础上又进行了一次封装,使用户可以更方便的使用。

在本次实验中确实印证了使用cdev比较复杂,且加载ko模块驱动之后还需要查看设备号,手动mknod节点,而且在卸载驱动的时候也是非常繁琐的,但在这里本着学习的目的也进行了实验,后续的开发会使用miscdevice机制而不使用cdev机制

本次实验主要针对字符设备的:

  • cdev注册设备
  • read函数的使用
  • write函数的使用

在上一篇博客基于OMAPL138的字符驱动_GPIO驱动AD9833(一)之ioctl,只能用ioctl函数进行一个字节的幻数进行指令通信,但无法传输类似于设置频率指令。如果传递这样的参数,只需要使用write和read函数完成数据的传递。

1. cdev的使用

cdev的定义

cdev的定义信息包含在#include <linux/cdev.h>头文件中,需要使用cdev当然要定义cdev的结构体了,我们将cdev的信息定义在了我们的设备定义struct ad9833下。

AD9833 结构体定义:

struct ad9833_t {

	struct ad9833_hw_t hw;
	struct ad9833_t *self;
	enum ad9833_wavetype_t wave_type;

	struct	cdev	cdev;
	unsigned char	mem[ AD9833_SIZE ];

	unsigned int delay;

	void (*write_reg)	( AD9833 *self, unsigned int reg_value);
	void (*init_device)	( AD9833 *self );
	void (*set_wave_freq)( AD9833 *self , unsigned long freqs_data);
	void (*set_wave_type)( AD9833 *self, enum ad9833_wavetype_t wave_type );
	void (*set_wave_phase)( AD9833 *self, unsigned int phase );
	void (*set_wave_para)( AD9833 *self, unsigned long freqs_data, unsigned int phase, enum ad9833_wavetype_t wave_type );
};

结构体内的struct cdev cdev就为我们使用的cdev目的就是向Linux内核申请自己的位置。

创建主设备号和次设备号

使用cdev需要向内核申请一个空间,则需要有一个主设备号提交给内核,我们可以使用Linux内核提供的一套宏函数来进行设备好的申请。通常的做法在设备init的函数里面。

MK_MAJOR( major, minor ); major 主设备号和 minor 次设备号,同款型的第二个设备次设备就是 2 以此类推。

#define				AD9833_MAJOR				230
dev_t devno;
devno    =   MKDEV( AD9833_MAJOR, 0 );

这个号码在我们mknod的时候比如,#mknod /dev/AD9833-ADI c 230 0 这个地方就会用到了。

cdev注册

int register_chrdev_region( dev_t from, unsigned int size, const char *name );

int alloc_chrdev_region( dev_t *dev, unsigned baseminor, unsigned count );

两个函数完成注册,第一个用于已知设备号的情况下,alloc那个用于未知设备号的,他会帮你分配设备号码。这里我们当然使用register_chrdev_region,里面第一个参数dev_t from就是我们上一个定义的dev_t devno = MKDEV(..)那个。

cdev初始化程序

dev_t	devno;
static int __init ad9833_dev_init( void )
{
	int  	i,ret;
	int  	index_minor = 0;
	int 	mk_major;

	/*
	 * cdev alloc and release device code.
	 * */
	devno = MKDEV( ad9833_major, index_minor );
	mk_major	=	MKDEV(ad9833_major, 0);
	if( ad9833_major ) {
		ret = register_chrdev_region( devno, 1, DRV_NAME );
	}else {
		ret = alloc_chrdev_region( &devno, 0, 1, DRV_NAME );
		ad9833_major	=	MAJOR(devno);
	}
	if( ret < 0 ) {
		printk(DRV_NAME "\t cdev alloc space failed.\n");
		return ret;
	}
	/*
	 * AD9833 new device
	 * */
	printk( DRV_NAME "\tApply memory for AD9833.\n" );
	ad9833 = ad9833_dev_new();
	if( !ad9833 ) {
		ret = -ENOMEM;
		printk(DRV_NAME "\tad9833 new device failed!\n" );
		goto fail_malloc;
	}

	/*
	 * AD9833 init gpios.
	 * */
	printk( DRV_NAME "\tInititial GPIO\n" );

	for ( i = 0; i < 3; i ++ ) {
		ret	=	gpio_request( ad9833_gpios[i], "AD9833 GPIO" );
		if( ret ) {
			printk("\t%s: request gpio %d for AD9833 failed, ret = %d\n", DRV_NAME,ad9833_gpios[i],ret);
			return ret;
		}else {
			printk("\t%s: request gpio %d for AD9833 set succussful, ret = %d\n", DRV_NAME,ad9833_gpios[i],ret);
		}
		gpio_direction_output( ad9833_gpios[i],1 );
		gpio_set_value( ad9833_gpios[i],0 );
	}

	/*
	 * cdev init.
	 * */
	cdev_init( &ad9833->cdev, &ad9833_fops );
	ad9833->cdev.owner	=	THIS_MODULE;
	ret = cdev_add( &ad9833->cdev, mk_major,1 );
	if( ret ) {
		printk( KERN_NOTICE "Error %d adding ad9833 %d", ret, 1 );
		return ret;
	}

	//ret = misc_register( &ad9833_miscdev );
	printk( DRV_NAME "\tinitialized\n" );
	return 0;

	fail_malloc:
	unregister_chrdev_region( mk_major,1 );
	return ret;

}

cdev的释放设备

rmmod之后设备要进行释放,这个地方必须正确释放,否则我们下载安装模块的时候只能重启。
void unregister_chrdev_region( dev_t from, unsigned count ) ,进行设备的释放。

static void __exit ad9833_dev_exit( void )
{
	int i;
	for( i = 0; i < 3; i++) {
		gpio_free( ad9833_gpios[i] );
	}
	//misc_deregister( &ad9833_miscdev );
	unregister_chrdev_region( devno,1 );

}

cdev设备的使命就完成了。

2. file read write操作

需要在file_operations结构体里面指定read和write函数:

file_operations结构体参数:

static struct file_operations ad9833_fops = {

		.owner				=	THIS_MODULE,
		.read				=  	ad9833_driver_read,
		.write				=	ad9833_driver_write,
		.unlocked_ioctl  	=  	ad9833_ioctl,
};

这里面ad9833_driver_read和ad9833_driver_write函数就指定了读写函数。**这里有个对应问题,正常思维是用户的write函数对应内核驱动的read函数,用户的read函数对应内核驱动的write函数,但这里面,用户的read函数对应的是内核的read函数,用户的write函数也是对应内核的write函数。**所以,当用户写应用程序write数据的时候,我们应该在ad9833_write函数里面读取这个数据处理,当对方read的时候,我们需要在ad9833_read里面进行处理read事件。

read函数

static ssize_t
ad9833_driver_read( struct file *filp, const char __user *buffer, size_t size, loff_t *f_pos )
{
	unsigned long 	p		=	*f_pos;
	unsigned int 	count	=	size;
	int 			ret		=	0;

	if ( p >= AD9833_SIZE )
		return 0;
	if ( count > AD9833_SIZE - p )
		count = AD9833_SIZE - p;
	if ( copy_to_user( buffer, ad9833->mem + p, count) ) {
		ret	=	-EFAULT;
	}else {
		*f_pos += count;
		ret = 	count;
		printk( DRV_NAME "\tread %u bytes from %lu\n", count, p );
	}
	return ret;
}

这里有个特殊的处理,copy_to_user函数,对于用户传递进来的指针,对其直接进行读取写入很危险的,所以这里使用copy_to_user把数据传递给用户,比较安全。

write函数

static ssize_t
ad9833_driver_write( struct file *filp, const char __user *buffer, size_t size, loff_t *f_pos )
{
	unsigned long 	p		=	*f_pos;
	unsigned int 	count	=	size;
	int 			ret		=	0;

	if ( p >= AD9833_SIZE )
		return 0;
	if ( count > AD9833_SIZE - p )
		count = AD9833_SIZE - p;

	memset( ad9833->mem,0, AD9833_SIZE );

	if ( copy_from_user( ad9833->mem + p, buffer, count) ) {
		ret	=	-EFAULT;
	}else {
		*f_pos += count;
		ret = 	count;
		printk( DRV_NAME "\twrite %u bytes from %lu\n", count, p );
		printk( DRV_NAME "\tRecv: %s \n", ad9833->mem + p );
		printk( DRV_NAME "\tSet freq is: %d \n", simple_strtol(ad9833->mem + p,"str",0) );
		ad9833->set_wave_freq(ad9833, simple_strtol(ad9833->mem + p,"str",0) );
	}
	return ret;
}

同理,直接操作用户传递进来的指针,很危险的,在write函数里copy_from_user进行数据转移交换,完成处理。这个write函数里面,用户通过write函数向驱动写入指令信息,然后解析出来,得到频率控制字,完成运算。

运行程序

把内核文件uImage拷贝到目标板子,把ad9833.ko文件也拷贝到目标板。

1) 加载驱动

#insmod ad9833.ko

2) 查看驱动挂载情况

#cat /proc/devices

3) 制作设备节点

#mknod /dev/AD9833-ADI c 230 0

就可以看见/dev/AD9833-ADI的节点了。

4) 运行测试程序

/*
CROSS=arm-none-linux-gnueabi-
all: ad9833_test
ad9833_test: ad9833_test.c
	$(CROSS)gcc -o ad9833_test.o ad9833_test.c -static
clean:
	@rm -rf ad9833_test *.o
 * */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

#define				AD9833_MAGIC				'k'
#define				CMD_TYPE_SIN				_IO( AD9833_MAGIC, 0)
#define				CMD_TYPE_TRI				_IO( AD9833_MAGIC, 1)
#define				CMD_TYPE_SQE				_IO( AD9833_MAGIC, 2)


const char dev_path[]="/dev/AD9833-ADI";

int main(int argc , char *argv[])
{

	int fd = -1, i = 0;

	printf("ad9833 test program run....\n");


	fd = open(dev_path, O_RDWR|O_NDELAY);  // 打开设备
	if (fd < 0) {
		printf("Can't open /dev/AD9833-ADI\n");
		return -1;
	}

	printf("open device.\n");

	if( strcmp(argv[1],"1") == 0 ) {
		ioctl(fd, CMD_TYPE_SIN, 5);
		printf("argc = %d,sine wave = %s \n", CMD_TYPE_SIN, argv[1]);
	}else if(  strcmp(argv[1],"2") == 0 ) {
		ioctl(fd, CMD_TYPE_TRI, 1);
		printf("argc = %d,tri wave = %s \n", CMD_TYPE_TRI,argv[1]);
	}else{
		ioctl(fd, CMD_TYPE_SQE, 1);
		printf("argc = %d,sqe wave = %s \n", CMD_TYPE_SQE, argv[1]);
	}
	write(fd, argv[2], strlen(argv[2]));

	printf("argc = %d\n", argc);
	close(fd);
	return 0;
}

编译成.o文件运行:

#mknod /dev/AD9833-ADI c 230 0

得到效果。

源代码下载

链接: https://pan.baidu.com/s/1lioLal_mvnbONFLQCBRF7w 密码: 5ptq

参考文献

[1] xiaobu1990, linux 字符设备和misc设备
, 2014年10月15日

12_ARMv8_异常处理(三)- GICv1/v2中断处理

12_ARMv8_异常处理(三)- GICv1/v2中断处理

0 前言

曾经在bare-mental环境开发DSP,STM32,使用C语言注册中断之后然后就可以工作了,并没有探究过本质,最多是配置一下中断相关的寄存器,而到了学习ARCH的阶段,所有掌握的重点并不是中断如何使用和中断使用注意事项,而应该站在更底层的角度去看待,中断到底如何产生,还有中断在操作系统中如何处理的。因此本节的学习目标,以ARM64的GIC设计为基础,学习GIC的设计**、使用方法还有Linux/freeRTOS操作系统如何去适配的,并且以树莓派4b实实在在的硬件去配置GIC,并对比TI和NXP这些二级厂商的GIC设计。

早期的ARM系统(ARM7/ARM9),采用单核处理器设计,中断较少,使用IER和ISR寄存器就能管理中断,这就是上一节我们提到的Legacy中断模式。随着处理器越来越复杂,多核出现,SoC设计也是越来越复杂,中断难以管理和集成,因此就需要专门设计一种机制来管理中断,这就是ARM公司设计的GIC的初衷。

GIC也并不是一成不变的,没有人能够预测未来会有什么需求,因此GIC也要随着时间去成长,现在目前最新版是GIC-V4 (IP core名:GIC-700)1 ,里面表格是对GIC一些发展史的总结:

版本 GIC-V1 GIC-V2 GIC-V3 GIC-V4
功能 8核,1020个中断源,8位二进制数优先级,支持软件出发中断源,支持trustzone 支持虚拟化,改进安全软件的支持 支持CPU大于8核,基于消息的中断,更多的中断ID 支持注入虚拟中断
IP核心 GIC-390 GIC-400 GIC-500 / 600 GIC-700
应用场景 Cortex-A9 MPcore Cortex-A7/A9 MPCore Cortex-A76 MPCore

1 GIC中断硬件设计

1.2 GICv2设计

1.2.1 GICv2架构设计
soc位置

GIC-400在SoC上的位置如图所示,GIC-400的输入是一对中断信号,还有一个可编程的接口,输出是一堆FIQ和IRQ。简言之,GIC-400就是对一堆中断信号最后整理成FIQ和IRQ的系统。从一个系统角度而言,GIC-400没有对输入进行同步化处理,因此在做一些时序比较强的场景时候,输入信号(SPI PPI)必须和CLK进行同步。

image-20220420160005487

  • SGI是用于多核通信之间的中断,支持16个SGI,中断号0~15,在linux内核中唤作IPI。(CPU私有)
  • PPI是每个处理器内核私有的中断,gic-400支持16个ppi,硬件中断号为16~31。(CPU私有)
  • SPI是公用的外设中断。GIC-400支持988个外设中断,硬件终端号范围32~1019。(CPU共享)

中断类型总结为下表:

INTID Interrupt Type Notes
0 - 15 SGIs Banked per PE
16 - 31 1056 - 1119 (GICv3.1) PPIs Banked per PE
32 - 1019 4096 - 5119 (GICv3.1) SPIs -
1020 - 1023 Special interrupt number Used to signal special cases, see Settings for each PE for more information.
1024 - 8191 Reserved -
8192 and greater LPIs The upper boundary is IMPLEMENTATION DEFINED
内部结构

image-20220420162203433

这里面一个共5个部件,distributor和cpu interface是关注的重点。

  • distributor:接收中断信号并且提交最高优先级的中断给CPU接口。而且分发器还受到IGR(interrupt Group Registers)的影响,IGR能够控制配置中断作为Group0或者Group1。(中断组影响后续CPU接口处理中断的路由,间接性地可以决定信号是作为一个FIQ或者IRQ异常请求),分发器是允许使用寄存器配置的。
  • cpu interface:cpu接口信号可以中断相应的处理器,并且可以接收来自于对应处理器的EOI访问请求和应答。这些AXI访问携带着中断号和其他关于中断的信息,并且切换distributor的状态机。当一个中断有足够的优先级的时候,CPU接口仅仅向处理器core通知中断pending。中断的优先级在CPU接口上可配。

其他一些部件是关于虚拟化的,暂时这里不涉及。

1.2.2 GICv2状态机

在GIC内部,中断信号有以下几种状态:

  • inactive,未激活状态:中断没有被asserted
  • pending,等待状态:中断已经被asserted,但是中断还没有被PE识别。
  • active,激活状态:中断已经被asserted,中断被PE识别。
  • active and pending,中断已经被PE识别,另一个同样的中断asserted

Note1, PE是processor element, 这里视为cpu core

Note2, 被PE识别的意思是,Receives acknowledge and EOI accesses from that processor

image-20220420175646182

外设中断的触发类型与状态机转换的问题:

  • edge-triggered: 上升沿或下降沿触发中断

    image-20220421090644591
  • level-triggered: 高低电平触发中断。

    image-20220421090721375
1.2.3 GICv2工作流程
flowchart TD
    A[assert an interrupt] --> B[GIC:标记为pending]
    B --> C{判断优先级} --最高-->D[发送CPU接口]
		  C--不满足--> B
	D --满足优先级要求--> E[发给CPU中断请求]
	E --> F[CPU进入异常中断读取CICC_IAR响应中断]
	F --> G{CPU仲裁}
	G --原来是pending状态--> H[设定为active]
	G --原来已经产生--> I[设定为AP状态]
	G --原来是active状态 --> J[设为为AP状态]
	H --> P[处理器完成中断服务发送EOI信号给GIC]
	I --> P
	J --> P
Loading

GIC-400提供的中断时序图,设定N中断优先级高于M中断,这个是一个电平触发的中断时序图:

image-20220421094832144

  • M中断和N中断在T0时刻都是idle状态,在T1时刻M中断被assert
  • M中断目前没有被PE响应,因此状态机是pending。
  • T17时刻nFIQCPU[n]信号被拉低(也就是说,在M中断信号进来之后大概15个时钟周期,nFIQCPU信号就会被拉低,nFIQCPU信号用于向CPU报告中断请求的,不过在此时CPU还没有响应M中断,该中断一直处于pending状态)
  • 不巧,T42时刻更高优先级的N中断进来了,N中断也没有被PE响应,因此也处于pending状态。
  • T58时刻,N中断发生大概15个时钟周期,nFIQCPU[n]拉低通知CPU(即便是nFIQCPU之前在T17时刻处于低电平,也不影响N中断的中断信息传递,因为CPU接口更新GICC_IAR寄存器的ID字段为中断N的中断号,CPU见到电平拉低会去读取GICC_IAR寄存器)
  • T61时刻,CPU发出ack-N信号,代表着CPU已经影响应了中断N,这时候分发器转换N中断为AP状态。Linux内核中断服务程序读取GICC_IAR的值,分发器把中断设定为active状态。这中间涉及了很多软件需要去做的事情:
    • T64,中断N被Linux内核响应, CPU完成对nFIQCPU的复位
    • T126,外设input source开始拉低。
    • T128,中断N退出等待状态。
    • T131,处理器(Linux内核中断服务程序)把中断N的硬件ID写入GICC_EOIR寄存器来完成整个过程。
  • T146,开始切回M中断,重复之前的处理。
1.2.4 GICv2寄存器

ARM提供了一些GIC-V2的通用寄存器:

  • 一部分是分发器寄存器,以GICD_开头
  • 一部分是CPU接口寄存器,以GICC_开头
global settings2

分发器控制寄存器必须被配置用于使能中断组和设定路由模式:

  • 使能Affinity routing(ARE) : 设定GIC工作在GICv2模式,在GICv3里面是配置GICv3或者是legacy(兼容GICv2)的。
  • GICD_CTRL,这个对Group0, secure Group1和 non-secure group 1 使能配置:
    • EnableGrp1S 使能Secure group 1
    • EnableGrp1NS 使能Nonsecure group 1
    • EnableGrp0 使能Group 0中断
settings for each PE
  • CPU Interface Configuration

    • 使能系统寄存器访问 ICC_*_ELn
    • 设定优先级和二进制点寄存器,ICC_PMR_EL1和ICC_BPRn_EL1。
    • 设定EOI模式,ICC_CTRL_EL1和ICC_CTRL_EL3。
    • 使能每个中断组的发射信号
      • ICC_IGRPEN1_EL1
      • ICC_GRPEN1_EL1
  • PE配置

    • routing controls

      SCR_EL3和HCR_EL1,这个路由控制位决定中断发生在哪个异常等级,必须被软件初始化。

    • interrupt masks

      PE必须异常mask位在PSTATE。

    • vector table

      VBAR_ELn寄存器必须配置。

  • SPI,PPI和SGI配置

    • 使用分发器的GICD_*配置SPIs

      image-20220421104850665
    • Priority: GICD_IPRIORITYn, GICR_IPRIORITYn

    • Group: GICD_IGROUPn, GICD_IGRPMODn, GICR_IGROUPn, GICR_IGRPMODn

    • Edge-triggered or level-sensitive: GICD_ICFGRn, GICR_ICFGRn

    • Enable: GICD_ISENABLERn, GICD_ICENABLER, GICR_ISENABLERn, GICR_ICENABLERn

    • Non-maskable: Interrupts configured as non-maskable are treated as higher priority than all other interrupts belonging to the same Group. That is, a non-maskable Non-secure Group 1 interrupt is treated as higher priority than all other Non-secure Group 1 interrupts.

      • The non-maskable property is added in GICv3.3 and requires matching support in the PE.
      • Only Secure Group 1 and Non-secure Group 1 interrupts can be marked as non-maskable.
  • 为SPIs配置目标PE

    • GICD_IROUTERn.Interrupt_Routing_Mode == 0

      The SPI is delivered to the PE A.B.C.D, which are the affinity co-ordinates specified in the register.

    • GICD_IROUTERn.Interrupt_Routing_Mode == 1

      The SPI can be delivered to any connected PE that is participating in distribution of the interrupt group. The Distributor, rather than software, selects the target PE. The target can therefore vary each time the interrupt is signaled. This type of routing is referred to as 1-of-N.

1.3 三方GICv2设计

1.3.1 bcm2711

bcm2711集成了GIC-400,基于GIC-400架构。GIC-400中断号的分配和SoC设计有关。

image-20220421124635870

如果在bcm2711访问中断寄存器,基地址0xFF840000。bcm2711在0xFF840000地址挂载的GIC中断控制器,然后是ARM的GIC中断控制器的地址:

Address range GIC-400 functional block
0x0000-0x0FFF Reserved
0x1000-0x1FFF Distributor
0x2000-0x3FFF CPU interfaces
0x4000-0x4FFF Virtual interface control block, for the processor that is performing the access
0x5000-0x5FFF Virtual interface control block, for the processor selected by address bits [11:9], 0x5000-0x51FF``0x5200-0x53FF...0x5E00-0x5FFF ,
0x6000-0x7FFF Virtual CPU interfaces

每个寄存器类还有自己的偏移地址3,参考引用文献。

1.3.2 nxp-imx.84

这个是nxp imx.8使用GIC的设计,在SoC的结构设计上,通常自己会堆叠一些逻辑,然后作为外围中断的输入到GIC,NXP还设计了IRQ Steer,由于是大小核设计,这里面不只是同一个架构的core,还包含了中断的多路复用器,使用二级厂商的芯片除了要注意配置ARM原生的GIC之外,还要注意配置二级厂商设计的RTL模块的配置,及这些模块和GIC之间的一些耦合关系。

image-20220421131648308

1.2.3 ti TDA4VM

TI设计的也类似,里面增加更复杂的总线仲裁机制和逻辑判断。现在我们也知道了,这些厂商拿公版的ARM做了什么处理。

image-20220421131452917

2 GIC中断kernel设计

ARM的GIC的RTL设计,由哪个CPU来处理中断,是GIC内部的distributor机制仲裁分发给GIC的CPU interface[n]的,这里面的路由过程是由SoC设计者编好号分好组,除了ARM声明的Core级的中断预留外,其他的中断都是需要配置GICD_IROUTERn来决定由哪个PE来处理,而且还要限定Secure/Non-secure中断组。这就有两种场景,

第一种两种同类型的目标PE中断几乎同时assert,这个时候distributor会按照优先级给给CPU interface[n]通知,并且把高优先级的中断号写在GICC_IAR的ID字段,次优先级的中断挂pending,此时从Linux角度就可以读到一个中断ID,进行Linux的handler处理,处理完之后Linux handler向GICC_EOIR寄存器写这个处理完的中断ID,然后GIC的distributor开始处理次一级的中断。 Linux角度是串行处理。

第二种两种中断目标PE不同,同时assert,这个时候两个中断 distirbutor认为是两个不影响的中断,只操作自身的寄存器,中断也会被通知到相应的PE。Linux角度来看,Linux并不支持嵌套中断,否则栈会被吃空。此时,中断上半部快速序串行处理,处理第一个中断的时候,linux会mask掉中断,然后建立中断下半部的tasklet或者工作队列,紧接着开中断再处理第二个中断,建立第二个中断的下半部。

简言之,同一个PE的多个中断,排队是由GIC分发器决定的;不同PE的不同中断,排队是由Linux决定的。

我们按照5的Linuxkernel分析思路对GIC在Linux上面的源代码进行分析。由于篇幅关系,对于Linux Kernel这块,我们放在Linux Kernel中进行展开讨论。

Reference

Footnotes

  1. [gic]-ARM gicv3/gicv4的详细介绍-2020/12

  2. Learn the architecture: Arm CoreLink Generic Interrupt Controller v3 and v4 Version 1.0

  3. CoreLink GIC-400 Generic Interrupt Controller Technical Reference Manual r0p1 - Distributor register summary

  4. i.MX 8QuadMax Applications Processor Reference Manual

  5. linux kernel的中断子系统之(七):GIC代码分析

嵌入式Linux编译内核步骤 / 重点解决机器码问题 / 三星2451

嵌入式系统更新内核

1. 前言

手里有一块Friendly ARM的MINI2451的板子,这周试着编译内核,然后更新一下这个板子的Linux内核,想要更新Linux Kernel 4.1版本,但是种种原因实在是没有更新成功;于是使用Friendly ARM板子提供的3.6版本的内核,但是他们的内核全都配置好了,你只需要按照常规的方法进行编译就好了,貌似不能更深入的理解内核, 后来我从kernel.org官网上下载原版内核,然后一点点的把2451这个板子需要文件移植过去,可谓是问题百出啊,也学习到了很多东西。

2. 准备材料

  • FriendlyARM 的mini2451板子一块
  • 使用的bootloader是FriendARM提供的闭源Superboot2451.bin
  • Linux3.6内核源代码(在官网下的,纯净的)
  • mini2451提供的.config文件

3. 烧写内核的几个重点

3.1 拿到全新内核的几个步骤

我们拿到全新内核的时候,一定要注意几个步骤,好像并没有几个书上由我写的这么详细的,基本上都是给一个大体的思路,但是到了真刀真枪上场的时候,真的是问题百出。以下讲围绕这几个方面进行讨论。

a) 修改顶层Makefile

b) Machine ID的处理( 在arch/arm/ 的mach中增加C文件 -> 修改Kconfig -> 修改Makefile文件 )

c) 在arch/arm/tools/mach-types 文件中增加Machine ID

3.1.1 修改顶层的Makefile

Makefile一共要修改2个地方即可:

  • 修改arch架构: ARCH ?= arm 注意,arm这几个后面不要打空格啊,要不然make的时候不识别。

    • 修改cross_compile 路径:CROSS_COMPILE?= /home/user/toolchian/arm-linux-

    注意1,CROSS_COMPILE的表述,我这里给的是全路径,而不是像书籍和网络博客上给的 arm-linux- ,这里为什么呢?因为你的电脑里面可能装了不是一个交叉编译环境的工具链,所以这里强烈推荐使用交叉编译环境的绝对路径,绝对不会出错,可以放心大胆的使用。

    注意2,arm-linux- 后面不要加空格,否则不会识别的,和ARCH那个选项一样,都不要有空格。 否则就会抛出:“ make: ** /home/delvis/work/linux-3.6/arch/arm: 是一个目录。 停止。”的错误。

Makefile只需要改这两个位置就可以了,大可保存。

3.1.2 为了Machine ID准备之修改C文件

路径就是 -> ./arch/arm/ 里面关于mach-"各种型号",你在教学视频或者书上经常看见如下的说明:

找一个和你板子型号相近的c文件然后复制一份出来。

我使用的是Linux3.2内核且,我的板子的型号是S3C2451的,所以很理所应当的找到mach-s3c24xx这个文件夹, ! 但是我发现在Linux4.1和Linux2.4版本的内核中没有mach-s3c24xx这个文件夹,其实没有什么关系,只要找到和你板子芯片型号相近的就可以的。

我这里是复制FriendlyARM提供的mach-mini2451.c这个文件,在这个文件需要注意几个地方,我现在还不是很明白这个文件是做什么的,看里面很多初始化的程序包括GPIO、时钟、定时器、中断等等,应该是对2451板子进行初始化的。暂时我还不会写,靠移植吧。

  • 文件末尾MACHINE_START( USER_DEFINE_STRING, "USER_DEFINE_STRING" ) { .... } 这个就是MACHINE id 的位置
  • 还有其中定义的函数,需要依赖很多头文件,原生内核里面没有,缺什么我就从FriendlyARM里面拷贝到相应的目录。

文件末尾的MACHINE_START传递的参数的USER_DEFINE_STRING就是我们一会儿要写入machine id的宏定义,我这里宏定义的字符串是MACH_MINI2451

3.1.3 修改mach-xxxx中的Kconfig文件

刚才刚刚添加了新的mach-mini2451.c的文件,就要在和这个文件同一个目录下的Kconfig中加入这个的配置项,我给出我的配置项:

config MACH_MINI2451
        bool "MINI2451BYDELVIS"
        #select S3C24XX_SMDK
        select S3C_DEV_FB
        select S3C_DEV_HSMMC
        select S3C_DEV_HSMMC1
        select S3C_DEV_NAND
        select S3C_DEV_USB_HOST
        select S3C2416_SETUP_SDHCI
        select WIRELESS_EXT
        select WEXT_SPY
        select WEXT_PRIV
        select AVERAGE
        help
          Say Y here if you are using an FriendlyARM MINI2451

在Kconfig中增加这个字符串,在顶层进行make menuconfig的时候,这个选项就会出来,我们在make menuconfig的时候,最先应该的就是选择板子的架构

  1. System Type --> ARM sytem type( Samsung S3C24XX Socs ) --> Samsung S3C24XX SoCs

选择后返回上一层

2)SASUNG S3C24XX SoCs Support ---> 先选上 SASUNG S3C2416/S3C2450 ---> 下面自动出 MINI2451BYDELVIS 一会儿说这个菜单的显示逻辑如何的

3)Exit 并且 Save 成为.config

这些菜单的逻辑和归属主要是Kconfig决定的,我们的config MACH_MINI2451这个条目要放到正确的位置,不是随便放一个位置就行,我们的每一个子菜单对于.config来说都是一项配置。

Kconfig中config MACH_MINI2451的位置

看图,首先注意两个画红圈的位置,我们讲config MACH_MINI2451这一坨放在了 if CPU_S3C2416 ..... endif这个里面,也就是说我们在make menuconfig中选择了”MINI2451BYDELVIS“这个选项的时候,MACH_MINI2451默认在.config文件CPU为S3C2416,(也可见,我们的mach-mini2451.c文件是复制mach-s3c2416.c文件改装而来了)

在make menuconfig菜单中,也只有选择S3C2416这个CPU型号,我们的这个选项才会出现

3.1.3 修改mach-s3c24xx文件下的Makefile文件

我们平白无故的增加了mach-mini2451.c文件,如果进行全局编译的时候,是不会进行编译的,因为没有makefile进行指导编译,所以我们需要修改这一层的Makefile,当我进行全局编译的时候,就有规则指导编译器进行编译。

# add by Carlos 2017.12.6
obj-$(CONFIG_MACH_MINI2451)             += mach-mini2451.o mini2451-lcds.o

在这一层的Makefile文件中随便找一个位置,然后按照这个格式输出文件。

3.1.4 增加机器码

说到机器码,我真的不得不吐槽一下Friendly ARM,在购买Friendly ARM的时候,购买的宣传界面,大篇幅的说他们花了重金开发了Superboot2451.bin企业级的bootloader,重点是闭源,bootloader还是不错的,从SD卡引导,支持串口,驱动支持的挺全的,还有一个他们独创的可视化界面miniTools的USBMODE一键安装系统,一键下载程序。**但是好歹闭源的同时,把bootloader的关键参数给出啊!!!!**我找了他们的Wiki,找了官网,手册,就是没有知道Superboot2451.bin的Machine ID,好歹把ID值给出来啊。

恩,后来经过实验输出,得到强大的闭源Superboot2451.bin的 机器码 machine id 是: 0x00000695

好了,吐槽到此为止,我还是建议自己开发uboot,这样方便。

只有bootloader和kernel中的机器码一样,内核才能正常启动,否则将会抛出:

Error: unrecognized/unsupported machine ID (r1 = 0x33f60264).

我们要么改正uboot中的ID,要么改正kernel中的ID,总之,无论数字是什么,要一样。

好了,Superboot2451.bin的既然是闭源的就没办法进行改正了,那么我们只能改正Kernel中的ID了,那么Kernel中的ID该怎么改正呢?

切换到: arch/arm/tools/文件夹,里面有个mach-types文件,我们在后面追加一个:

mini2451 MACH_MINI2451 MINI2451 0x695

细心的朋友已经发现了,在上面的章节中,零零散散的文件用的也是这些字符,也就是mini2451、MACH_MINI2451、MINI2451这个宏定义都是这个ID值。

到此我们的的内核配置完毕了。

3.2 make menuconfig配置

切换到最后一步,进行make menuconfig进行.config的配置,在韦东山老师的视频里面,提到这个.config文件来源于三个部分

  1. 进行make menuconfig配置后的,你需要一条条的进行配置。
  2. 使用默认配置,在上面修改
  3. 使用厂家提供的

我这里,使用厂家提供的mini2451_linux_config文件,在上面进行修改,首先就把运行把这个config文件复制过来,并且名字改为.config覆盖原有的.config文件,然后进行make menucofig,在system type中如同上面讲述的选择正确的板子的型号。如果你没有选择,就会出现这样的异常,在内核编译到最后的时候,出现编译kernel出现no machine record defined 错误。然后一些鬼一样的网站给出馊主意,还被大量的博客转载,简直就是误导人,你经过google或者百度,会有一个这样的解决方案

这里给一个!反!面!教!材!:

放狗搜后,按照如下方法可以解决。将arch/arm/kernel/vmlinux.lds的最后两行(如下),给注释起来,但都没说是为了什么
ASSERT((__proc_info_end - __proc_info_begin), "missing CPU support"),
ASSERT((__arch_info_end - __arch_info_begin), "no machine record defined")

反面教材来源:uncompressing linux .................................................后没反应解决办法 前半部分

简直是神一样的操作,出错了,注释起来就可以维持了,什么逻辑,简直是对技术的侮辱!!!我尝试过了,的确内核编译通过了,可以把内核传输到ARM上面,到Load Kernel的下一步,就抛出了Error: unrecognized/unsupported machine ID (r1 = 0x33f60264). 的异常,显然是没有机器码没有定义。

注释这个鬼方法根本就不能用好吗?谁出的馊主意!!

正确的解法是:

按照我们上面的章节进行配置,顺理成章的解决Machine ID的问题: ->

  1. Machine ID的处理( 在arch/arm/ 的mach中增加C文件 -> 修改Kconfig -> 修改Makefile文件 )

  2. 在arch/arm/tools/mach-types 文件中增加Machine ID

  3. 在make menuconfig的menu中选择system type,选择正确的型号,会自动配置到.config

3.3 make

然后开始make就可以了,编译内核,我一般都喜欢用make -j8 多线程编程,速度快,但是很烧CPU。。。

基本上都是这样的状态。。。看温度。

编译完成之后,按照Friendly ARM提供的文档,把zImage文件拷到SD卡正确的路径,然后从SD卡启动正常烧写就好了。

4 总结

看到这样的输出的时候,真的很激动,终于内核开始正常解压了。磨了人好几天。


第15周主要搞内核的搞完了,最近要忙着复习考试,内核事情可能要稍微缓缓了。以后再慢慢研究。

参考文献:

[1] 贵气的博客著.Error: unrecognized/unsupported machine ID (r1 = 0x33f60264)..新浪博客.2011-03-20.

[2]韦东山著.《嵌入式Linux完全开发手册-应用开发》书籍.

[3]FriendlyARM著.2451开发手册. 用户手册.


版权声明:

1. 本文为MULTIBEANS团队研发跟随文章,未经允许不得转载。

2· 文中涉及的内容若有侵权行为,请与本人联系,本人会及时删除。

3· 尊重成果,本文将用的参考文献全部给出,向无私的工程师,爱好者致敬。


07_ELF文件_堆和栈调用惯例以ARMv8为例

07_ELF文件_堆和栈调用惯例以ARMv8为例

1 栈与调用惯例

1.1 栈的概念

栈和堆的概念非常重要,程序员的修养是以x86架构讲的堆栈的概念,我们以ARMv8 AArch64为主来研究一下堆栈。

fa0e9d749b2a79012

栈的概念我们可以重力翻转之后的桌子上的一摞书为例子,栈顶就是最下面眼镜的位置,栈底就是桌子。栈的顺序就是我们最后放的眼镜,是先被拿出来的。栈(stack)是一种数据结构,计算机里面的栈使用栈数据结构管理内存。为什么要将“重力翻转”?因为栈是一种从高地址向低地址生长的存储结构,栈底对应高地址,栈顶对应低地址。

image-20220419103804560

这里的SP被称为“堆栈帧(Stack Frame)”或者“活动记录(Activate Record)”。堆栈帧会保存以下记录:

  • 函数返回地址和参数,如果传递参数≤8个,那么使用X0~X7通用寄存器来传递,当参数多于8个,需要使用栈来传递参数。
  • 临时变量,例如局部变量
  • 保存上下文

1.2 不同架构出栈和入栈

入栈过程:

telegram-cloud-photo-size-5-6266933850319990826-y

A32指令集提供了PUSH和POP指令来实现入栈和出栈1,但是A64指令集已经去掉了PUSH和POP指令,只需要复用stp和ldp指令就可以实现入栈和出栈2

For example:

// Broken AArch64 implementation of `push {x1}; push {x0};`.
  str   x1, [sp, #-8]!  // This works, but leaves `sp` with 8-byte alignment ...
  str   x0, [sp, #-8]!  // ... so the second `str` will fail.

In this particular case, the stores could be combined:

// AArch64 implementation of `push {x0, x1}`.
  stp   x0, x1, [sp, #-16]!

However, in a simple compiler, it is not always easy to combine instructions in that way.

If you're handling w registers, the problem will be even more apparent: these have to be pushed in sets of four to maintain stack pointer alignment, and since this isn't possible in a single instruction, the code can become difficult to follow. This is what VIXL generates, for example:

// AArch64 implementation of `push {w0, w1, w2, w3}`.
  stp   w0, w1, [sp, #-16]!   // Allocate four words and store w0 and w1 at the lower addresses.
  stp   w2, w3, [sp, #8]      // Store w2 and w3 at the upper addresses.

这里AArch64实现入栈和出栈操作:

.globalmain
main:
		/* 栈往下扩展16个字节 */
		stp x29, x30, [sp, #-16]!
		
		/* 把栈继续往下扩展8字节 */
		add sp, sp, #-8
		mov x8, #1
		/* 保存x8到SP */
		str x8, [sp]
		
		/* 释放刚才扩展的8字节的栈空间 */
		add sp, sp, #8
		
		/* main函数返回0 */
		mov w0, 0
		
		/* 恢复x29和x30寄存器的值,使SP指向原位置 */
		ldp x29, x30, [sp], #16
		ret

1.3 fomit-frame-pointer

使用aarch64-none-elf-gcc编译器参数-fomit-frame-pointer可以取消帧指针:

  • 好处:不使用任何帧指针,直接计算变量的位置
  • 坏处:无法trace,寻址变慢

image-20220419122840960

使用fomit-frame-pointer的反汇编可以看到,123行sp已经不会备份到x29。

1.4 调用惯例Call convention

函数调用方和被调用方需要按照统一的协议去压栈和出栈,否则会有问题。调用惯例

  • 函数参数的传递顺序和方式
  • 栈的维护方式
  • 名字修饰,默认是 _cdecl __attribute__((cdecl))

1.4.1 函数参数压栈和出栈

我们定义一个这样的函数,有30个参数,看看arm编译器如何处理参数的压栈和出栈,另外对参数的类型也需要有观察。

static int s11( long a1,
                char a2,
                int a3,
                int a4,
                int a5,
                int a6,
                int a7,
                int a8,
                int a9,
                int a10
                )
{
    return  \
    a1 +    \
    a2 +    \
    a3 +    \
    a4 +    \
    a5 +    \
    a6 +    \
    a7 +    \
    a8 +    \
    a9 +    \
    a10;
}

int call_stack(void) {
    int a = s11(1,2,3,4,5,6,7,8,9,10);
    return a;
}

这段函数的反汇编是:

A64: ARMv8 AArch64
00000000000000d0 <s11>:
  d0:   d100c3ff        sub     sp, sp, #48
  d4:   f90017e0        str     x0, [sp, #40] 		//a1
  d8:   39009fe1        strb    w1, [sp, #39]			//a2
  dc:   b90023e2        str     w2, [sp, #32]			//a3
  e0:   b9001fe3        str     w3, [sp, #28]			//a4
  e4:   b9001be4        str     w4, [sp, #24]			//a5
  e8:   b90017e5        str     w5, [sp, #20]     //a6
  ec:   b90013e6        str     w6, [sp, #16]     //a7
  f0:   b9000fe7        str     w7, [sp, #12]     //a8
  f4:   39409fe0        ldrb    w0, [sp, #39]     // load a2 from stack
  f8:   f94017e1        ldr     x1, [sp, #40]     // load a1 from stack
  fc:   0b010001        add     w1, w0, w1        // a1 + a2
 100:   b94023e0        ldr     w0, [sp, #32]     // +a3
 104:   0b000021        add     w1, w1, w0
 108:   b9401fe0        ldr     w0, [sp, #28]     // +a4
 10c:   0b000021        add     w1, w1, w0
 110:   b9401be0        ldr     w0, [sp, #24]     // ....
 114:   0b000021        add     w1, w1, w0
 118:   b94017e0        ldr     w0, [sp, #20]
 11c:   0b000021        add     w1, w1, w0
 120:   b94013e0        ldr     w0, [sp, #16]
 124:   0b000021        add     w1, w1, w0
 128:   b9400fe0        ldr     w0, [sp, #12]
 12c:   0b000021        add     w1, w1, w0
 130:   b94033e0        ldr     w0, [sp, #48]     // load a9 from stack 
 134:   0b000021        add     w1, w1, w0        // +a9
 138:   b9403be0        ldr     w0, [sp, #56]     // load a10 from stack
 13c:   0b000020        add     w0, w1, w0        // +a10
 140:   9100c3ff        add     sp, sp, #48
 144:   d65f03c0        ret

0000000000000148 <call_stack>:
 148:   d100c3ff        sub     sp, sp, #48
 14c:   a9017bfd        stp     x29, x30, [sp, #16]
 150:   910043fd        add     x29, sp, #0x10
 154:   52800140        mov     w0, #0xa                        // #10
 158:   b9000be0        str     w0, [sp, #8]
 15c:   52800120        mov     w0, #0x9                        // #9
 160:   b90003e0        str     w0, [sp]
 164:   52800107        mov     w7, #0x8                        // #8
 168:   528000e6        mov     w6, #0x7                        // #7
 16c:   528000c5        mov     w5, #0x6                        // #6
 170:   528000a4        mov     w4, #0x5                        // #5
 174:   52800083        mov     w3, #0x4                        // #4
 178:   52800062        mov     w2, #0x3                        // #3
 17c:   52800041        mov     w1, #0x2                        // #2
 180:   d2800020        mov     x0, #0x1                        // #1
 184:   97ffffd3        bl      d0 <s11>
 188:   b9002fe0        str     w0, [sp, #44]
 18c:   b9402fe0        ldr     w0, [sp, #44]
 190:   a9417bfd        ldp     x29, x30, [sp, #16]
 194:   9100c3ff        add     sp, sp, #48
 198:   d65f03c0        ret
A32: ARMv7 AArch32
000000b0 <s11>:
  b0:   b480            push    {r7}
  b2:   b085            sub     sp, #20
  b4:   af00            add     r7, sp, #0
  b6:   60f8            str     r0, [r7, #12]
  b8:   607a            str     r2, [r7, #4]
  ba:   603b            str     r3, [r7, #0]
  bc:   460b            mov     r3, r1
  be:   72fb            strb    r3, [r7, #11]
  c0:   7afa            ldrb    r2, [r7, #11]
  c2:   68fb            ldr     r3, [r7, #12]
  c4:   441a            add     r2, r3
  c6:   687b            ldr     r3, [r7, #4]
  c8:   441a            add     r2, r3
  ca:   683b            ldr     r3, [r7, #0]
  cc:   441a            add     r2, r3
  ce:   69bb            ldr     r3, [r7, #24]
  d0:   441a            add     r2, r3
  d2:   69fb            ldr     r3, [r7, #28]
  d4:   441a            add     r2, r3
  d6:   6a3b            ldr     r3, [r7, #32]
  d8:   441a            add     r2, r3
  da:   6a7b            ldr     r3, [r7, #36]   ; 0x24
  dc:   441a            add     r2, r3
  de:   6abb            ldr     r3, [r7, #40]   ; 0x28
  e0:   441a            add     r2, r3
  e2:   6afb            ldr     r3, [r7, #44]   ; 0x2c
  e4:   4413            add     r3, r2
  e6:   4618            mov     r0, r3
  e8:   3714            adds    r7, #20
  ea:   46bd            mov     sp, r7
  ec:   f85d 7b04       ldr.w   r7, [sp], #4
  f0:   4770            bx      lr
  f2:   bf00            nop

000000f4 <call_stack>:
  f4:   b580            push    {r7, lr}
  f6:   b088            sub     sp, #32
  f8:   af06            add     r7, sp, #24
  fa:   2300            movs    r3, #0
  fc:   607b            str     r3, [r7, #4]
  fe:   2308            movs    r3, #8
 100:   603b            str     r3, [r7, #0]
 102:   230a            movs    r3, #10
 104:   9305            str     r3, [sp, #20]
 106:   2309            movs    r3, #9
 108:   9304            str     r3, [sp, #16]
 10a:   2308            movs    r3, #8
 10c:   9303            str     r3, [sp, #12]
 10e:   2307            movs    r3, #7
 110:   9302            str     r3, [sp, #8]
 112:   2306            movs    r3, #6
 114:   9301            str     r3, [sp, #4]
 116:   2305            movs    r3, #5
 118:   9300            str     r3, [sp, #0]
 11a:   2304            movs    r3, #4
 11c:   2203            movs    r2, #3
 11e:   2102            movs    r1, #2
 120:   2001            movs    r0, #1
 122:   f7ff ffc5       bl      b0 <s11>
 126:   6078            str     r0, [r7, #4]
 128:   687a            ldr     r2, [r7, #4]
 12a:   683b            ldr     r3, [r7, #0]
 12c:   4413            add     r3, r2
 12e:   4618            mov     r0, r3
 130:   3708            adds    r7, #8
 132:   46bd            mov     sp, r7
 134:   bd80            pop     {r7, pc}
 136:   bf00            nop

telegram-cloud-photo-size-5-6266933850319990918-y

前8个参数被压入寄存器中,后面的参数被直接压到栈中。返回参数被放在x0中,返回地址在x30中。 参考:02_ARMv7-M_编程模型与模式

1.4.2 函数调用压栈和出栈

static int s0(void) {
    return 0;
}

static int s1(void) {
    return s0();
}

static int s2(void) {
    return s1();
}
static int s3(void) {
    return s2();
}
static int s4(void) {
    return s3();
}
static int s5(void) {
    return s4();
}
static int s6(void) {
    return s5();
}
static int s7(void) {
    return s6();
}
static int s8(void) {
    return s7();
}
static int s9(void) {
    return s8();
}
static int s10(void){
    return s9();
}

int call_stack(void) {
    int a = 0;
    int c = 8;
    c = s10();

    return a + c;
}

反汇编:

A64: ARMv8 AArch64
Disassembly of section .text:

0000000000000000 <s0>:
   0:   52800000        mov     w0, #0x0                        // #0
   4:   d65f03c0        ret

0000000000000008 <s1>:
   8:   a9bf7bfd        stp     x29, x30, [sp, #-16]!
   c:   910003fd        mov     x29, sp
  10:   97fffffc        bl      0 <s0>
  14:   a8c17bfd        ldp     x29, x30, [sp], #16
  18:   d65f03c0        ret

000000000000001c <s2>:
  1c:   a9bf7bfd        stp     x29, x30, [sp, #-16]!
  20:   910003fd        mov     x29, sp
  24:   97fffff9        bl      8 <s1>
  28:   a8c17bfd        ldp     x29, x30, [sp], #16
  2c:   d65f03c0        ret

0000000000000030 <s3>:
  30:   a9bf7bfd        stp     x29, x30, [sp, #-16]!
  34:   910003fd        mov     x29, sp
  38:   97fffff9        bl      1c <s2>
  3c:   a8c17bfd        ldp     x29, x30, [sp], #16
  40:   d65f03c0        ret

0000000000000044 <s4>:
  44:   a9bf7bfd        stp     x29, x30, [sp, #-16]!
  48:   910003fd        mov     x29, sp
  4c:   97fffff9        bl      30 <s3>
  50:   a8c17bfd        ldp     x29, x30, [sp], #16
  54:   d65f03c0        ret

0000000000000058 <s5>:
  58:   a9bf7bfd        stp     x29, x30, [sp, #-16]!
  5c:   910003fd        mov     x29, sp
  60:   97fffff9        bl      44 <s4>
  64:   a8c17bfd        ldp     x29, x30, [sp], #16
  68:   d65f03c0        ret

000000000000006c <s6>:
  6c:   a9bf7bfd        stp     x29, x30, [sp, #-16]!
  70:   910003fd        mov     x29, sp
  74:   97fffff9        bl      58 <s5>
  78:   a8c17bfd        ldp     x29, x30, [sp], #16
  7c:   d65f03c0        ret

0000000000000080 <s7>:
  80:   a9bf7bfd        stp     x29, x30, [sp, #-16]!
  84:   910003fd        mov     x29, sp
  88:   97fffff9        bl      6c <s6>
  8c:   a8c17bfd        ldp     x29, x30, [sp], #16
  90:   d65f03c0        ret

0000000000000094 <s8>:
  94:   a9bf7bfd        stp     x29, x30, [sp, #-16]!
  98:   910003fd        mov     x29, sp
  9c:   97fffff9        bl      80 <s7>
  a0:   a8c17bfd        ldp     x29, x30, [sp], #16
  a4:   d65f03c0        ret

00000000000000a8 <s9>:
  a8:   a9bf7bfd        stp     x29, x30, [sp, #-16]!
  ac:   910003fd        mov     x29, sp
  b0:   97fffff9        bl      94 <s8>
  b4:   a8c17bfd        ldp     x29, x30, [sp], #16
  b8:   d65f03c0        ret

00000000000000bc <s10>:
  bc:   a9bf7bfd        stp     x29, x30, [sp, #-16]!
  c0:   910003fd        mov     x29, sp
  c4:   97fffff9        bl      a8 <s9>
  c8:   a8c17bfd        ldp     x29, x30, [sp], #16
  cc:   d65f03c0        ret

0000000000000148 <call_stack>:
 148:   a9be7bfd        stp     x29, x30, [sp, #-32]!
 14c:   910003fd        mov     x29, sp
 150:   b9001fff        str     wzr, [sp, #28]
 154:   52800100        mov     w0, #0x8                        // #8
 158:   b9001be0        str     w0, [sp, #24]
 15c:   97ffffd8        bl      bc <s10>
 160:   b9001be0        str     w0, [sp, #24]
 164:   b9401fe1        ldr     w1, [sp, #28]
 168:   b9401be0        ldr     w0, [sp, #24]
 16c:   0b000020        add     w0, w1, w0
 170:   a8c27bfd        ldp     x29, x30, [sp], #32
 174:   d65f03c0        ret

每个函数都在将sp - 16的位置,让栈向下增,栈空间逐步加大, 把x29和x30,栈指针和返回地址存入栈空间,然后函数返回后弹出栈。

A32: ARMv7 AArch32
Disassembly of section .text:

00000000 <s0>:
   0:   b480            push    {r7}
   2:   af00            add     r7, sp, #0
   4:   2300            movs    r3, #0
   6:   4618            mov     r0, r3
   8:   46bd            mov     sp, r7
   a:   f85d 7b04       ldr.w   r7, [sp], #4
   e:   4770            bx      lr

00000010 <s1>:
  10:   b580            push    {r7, lr}
  12:   af00            add     r7, sp, #0
  14:   f7ff fff4       bl      0 <s0>
  18:   4603            mov     r3, r0
  1a:   4618            mov     r0, r3
  1c:   bd80            pop     {r7, pc}
  1e:   bf00            nop

00000020 <s2>:
  20:   b580            push    {r7, lr}
  22:   af00            add     r7, sp, #0
  24:   f7ff fff4       bl      10 <s1>
  28:   4603            mov     r3, r0
  2a:   4618            mov     r0, r3
  2c:   bd80            pop     {r7, pc}
  2e:   bf00            nop

00000030 <s3>:
  30:   b580            push    {r7, lr}
  32:   af00            add     r7, sp, #0
  34:   f7ff fff4       bl      20 <s2>
  38:   4603            mov     r3, r0
  3a:   4618            mov     r0, r3
  3c:   bd80            pop     {r7, pc}
  3e:   bf00            nop

00000040 <s4>:
  40:   b580            push    {r7, lr}
  42:   af00            add     r7, sp, #0
  44:   f7ff fff4       bl      30 <s3>
  48:   4603            mov     r3, r0
  4a:   4618            mov     r0, r3
  4c:   bd80            pop     {r7, pc}
  4e:   bf00            nop

00000050 <s5>:
  50:   b580            push    {r7, lr}
  52:   af00            add     r7, sp, #0
  54:   f7ff fff4       bl      40 <s4>
  58:   4603            mov     r3, r0
  5a:   4618            mov     r0, r3
  5c:   bd80            pop     {r7, pc}
  5e:   bf00            nop

00000060 <s6>:
  60:   b580            push    {r7, lr}
  62:   af00            add     r7, sp, #0
  64:   f7ff fff4       bl      50 <s5>
  68:   4603            mov     r3, r0
  6a:   4618            mov     r0, r3
  6c:   bd80            pop     {r7, pc}
  6e:   bf00            nop

00000070 <s7>:
  70:   b580            push    {r7, lr}
  72:   af00            add     r7, sp, #0
  74:   f7ff fff4       bl      60 <s6>
  78:   4603            mov     r3, r0
  7a:   4618            mov     r0, r3
  7c:   bd80            pop     {r7, pc}
  7e:   bf00            nop

00000080 <s8>:
  80:   b580            push    {r7, lr}
  82:   af00            add     r7, sp, #0
  84:   f7ff fff4       bl      70 <s7>
  88:   4603            mov     r3, r0
  8a:   4618            mov     r0, r3
  8c:   bd80            pop     {r7, pc}
  8e:   bf00            nop

00000090 <s9>:
  90:   b580            push    {r7, lr}
  92:   af00            add     r7, sp, #0
  94:   f7ff fff4       bl      80 <s8>
  98:   4603            mov     r3, r0
  9a:   4618            mov     r0, r3
  9c:   bd80            pop     {r7, pc}
  9e:   bf00            nop

000000a0 <s10>:
  a0:   b580            push    {r7, lr}
  a2:   af00            add     r7, sp, #0
  a4:   f7ff fff4       bl      90 <s9>
  a8:   4603            mov     r3, r0
  aa:   4618            mov     r0, r3
  ac:   bd80            pop     {r7, pc}
  ae:   bf00            nop
    
000000f4 <call_stack>:
  f4:   b580            push    {r7, lr}
  f6:   b082            sub     sp, #8
  f8:   af00            add     r7, sp, #0
  fa:   2300            movs    r3, #0
  fc:   607b            str     r3, [r7, #4]
  fe:   2308            movs    r3, #8
 100:   603b            str     r3, [r7, #0]
 102:   f7ff ffcd       bl      a0 <s10>
 106:   6038            str     r0, [r7, #0]
 108:   687a            ldr     r2, [r7, #4]
 10a:   683b            ldr     r3, [r7, #0]
 10c:   4413            add     r3, r2
 10e:   4618            mov     r0, r3
 110:   3708            adds    r7, #8
 112:   46bd            mov     sp, r7
 114:   bd80            pop     {r7, pc}
 116:   bf00            nop

1.4.3 ARMv8的函数调用标准

函数调用标准(Procedure Call Standard, PCS)用来描述父/子函数是如何编译、链接的,尤其是父函数和子函数之间调用关系的约定,如栈的布局、参数的传递、还有C语言类型的长度等等。每个处理器体系结构都有不同的标准。下面以ARM64为例介绍函数调用的标准(参考: Procedure Call Standard for ARM 64-bit Architecture3 4 )

ARM64体系结构的通用寄存器:

寄存器 描述
SP寄存器 SP寄存器
x30 (LR寄存器) 链接寄存器
x29 (FP寄存器) 栈帧指针(Frame Pointer)寄存器
x19~x28 被调用函数保存的寄存器,在子函数中使用时需要保存到栈中。
x18 平台寄存器
x17 临时寄存器IPC(intra-precedure-call)临时寄存器
x16 临时寄存器或第一个IPC临时寄存器
x9~x15 临时寄存器
x8 间接结果位置寄存器,用于保存程序返回的地址
x0~x7 用于传递子函数参数和结果,

2 堆与内存管理

堆的概念我们已经知道了,而且我们还用过大名鼎鼎的malloc函数,甚至malloc_align函数,但是我们似乎没有研究过在Linux里面malloc原理是什么样子的,在今天的这个topic我们再进一步的了解一下堆,后面我们在学习linux内核的内存管理的时候会更详细的讲解一下malloc如何实现的。

2.1 Linux进程堆管理

Linux进程地址空间,除了文件、共享库还有栈之外,剩余的未分配的空间都可以作为Heap的空间地址,堆和栈相反,堆是向上增长的。运行库向操作系统申请一批空间地址,又程序自己“零售”给内部程序。

Linux进程堆管理有两种方式:

  • brk()系统调用
  • mmap()

brk()系统调用实际上就设置进程数据段(data段+bss段的统称)的结束地址,如果我们将数据段结束地址向高地址不断滚动,那么扩大的空间就是我们可以用的heap的空间,glibc里面有个sbrk函数。

mmap()的作用是向操作系统申请一段虚拟内存地址,如果指定文件路径是可以将空间映射到文件,如果没有指定文件路径,那么就是匿名空间(Anonymous),匿名空间就可以作为堆空间。mmap可以指定申请空间的大小和起始地址,如果起始地址设定为0,那么mmap会自动跳转到合适的位置,申请的空间还可以指定权限。

void *malloc(size_t nbytes)
{
     void *ret = mmap(0, bytes, PROT_READ|PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
     __check_ret__(ret);
     return ret;
}

glibc的malloc函数处理逻辑是这样的:

  • 对于小于128KB的请求,它会在现有的堆空间分配。
  • 对于大于128KB的请求,它会使用mmap函数为它分配一段匿名空间,然后再从匿名空间分配用户空间。

2.2 堆分配算法

  • 空闲链表法
  • 位图法
  • 对象池法

2.3 堆碎片化问题

2.3.1 碎片产生5

int main()
{
        int *heap_d;
        int *heap_e;
        int *heap_f;
        heap_d = (int *)malloc(10);
        heap_e = (int *)malloc(10);
        printf("The d address is %p\n",heap_d);
        printf("The e address is %p\n",heap_e);
        free(heap_d);
        heap_d = NULL;
        heap_f = (int *)malloc(30);
        printf("The f address is %p\n",heap_f);
        return 0;
}
The d address is 0xf0d010 mem_d
The e address is 0xf0d030 mem_e
The f address is 0xf0d460 mem_f
 
可想而知,总共三段内存分配
mem_d|mem_e|
free
     |mem_e|
           |mem_f|
|xxxx|     |     |
xxx为无用内存,碎片,即使分配后已经free和置NULL操作。
越来越多的malloc使用,会促进内存碎片化加剧,最终内存不足。

2.3.2 baremental/freeRTOS堆空间

嵌入式设备没有MMU,无法实现内存动态映射。所以没有操作系统兜底的嵌入式设备一定要小心,就算是有操作系统也要对内存分配了如指掌,否则就会出现意想不到的问题,内存碎片的问题就是很头疼的问题。

freeRTOS

freeRTOS对于堆的管理分为5个heap管理方式6,十分复杂。

  • heap_1 - the very simplest, does not permit memory to be freed.
  • heap_2 - permits memory to be freed, but does not coalescence adjacent free blocks.
  • heap_3 - simply wraps the standard malloc() and free() for thread safety.
  • heap_4 - coalescences adjacent free blocks to avoid fragmentation. Includes absolute address placement option.
  • heap_5 - as per heap_4, with the ability to span the heap across multiple non-adjacent memory areas.
baremental7

malloc和free并不能实现动态的内存的管理。这需要在启动阶段专门给其分配一段空闲的内存区域作为malloc的内存区。如STM32中的启动文件startup_stm32f10x_md.s中可见以下信息:

Heap_Size       EQU     0x00000800

                AREA    HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem        SPACE   Heap_Size
__heap_limit

其中,Heap_Size即定义一个宏定义。数值为 0x00000800。Heap_Mem则为申请一块连续的内存,大小为 Heap_Size。简化为C语言版本如下:

#define Heap_Size 0x00000800
unsigned char Heap_Mem[Heap_Size] = {
     0};

在这里申请的这块内存,在接下来的代码中,被注册进系统中给malloc和free函数所使用:

__user_initial_stackheap
LDR     R0, =  Heap_Mem  ;  返回系统中堆内存起始地址
LDR     R1, =(Stack_Mem + Stack_Size)
LDR     R2, = (Heap_Mem +  Heap_Size); 返回系统中堆内存的结束地址
LDR     R3, = Stack_Mem
BX      LR

在函数中使用malloc,如果是大的内存分配,而且malloc与free的次数也不是特别频繁,使用malloc与free是比较合适的,但是如果内存分配比较小,而且次数特别频繁,那么使用malloc与free就有些不太合适了。因为过多的malloc与free容易造成内存碎片,致使可使用的堆内存变小。尤其是在对单片机等没有MMU的芯片编程时,慎用malloc与free。

对于堆碎片化的问题,可以采用堆分配算法避免,比如内存池。

内存池,简洁地来说,就是预先分配一块固定大小的内存。以后,要申请固定大小的内存的时候,即可从该内存池中申请。用完了,自然要放回去。注意,内存池,每次申请都只能申请固定大小的内存。这样子做,有很多好处:

  • 每次动态内存申请的大小都是固定的,可以有效防止内存碎片化。(至于为什么,可以想想,每次申请的都是固定的大小,回收也是固定的大小)

  • 效率高,不需要复杂的内存分配算法来实现。申请,释放的时间复杂度,可以做到O(1)。

  • 内存的申请,释放都在可控的范围之内。不会出现以后运行着,运行着,就再也申请不到内存的情况。

内存池,并非什么很厉害的技术。实现起来,其实可以做到很简单。只需要一个链表即可。在初始化的时候,把全局变量申请来的内存,一个个放入该链表中。在申请的时候,只需要取出头部并返回即可。在释放的时候,只需要把该内存插入链表。以下是一种简单的例子(使用移植来的linux内核链表,对该链表的移植,以后有时间再去分析):

#define MEM_BUFFER_LEN  5    //内存块的数量
#define MEM_BUFFER_SIZE 256 //每块内存的大小

//内存池的描述,使用联合体,体现穷人的智慧。就如,我一同学说的:一个字节,恨不得掰成8个字节来用。
typedef union mem {
     
struct list_head list;
unsigned char buffer[MEM_BUFFER_SIZE];
}mem_t;

static union mem gmem[MEM_BUFFER_LEN];

LIST_HEAD(mem_pool);

//分配内存
void *mem_pop(){
     
    union mem *ret = NULL;
    psr_t psr;

    psr = ENTER_CRITICAL();
    if(!list_empty(&mem_pool)) { //有可用的内存池 
        ret = list_first_entry(&mem_pool, union mem, list);
        //printf("mem_pool = 0x%p  ret = 0x%p\n", &mem_pool, &ret->list);
        list_del(&ret->list);
 }
 EXIT_CRITICAL(psr);
 return ret;//->buffer;
}


//回收内存
void mem_push(void *mem){
     
    union mem *tmp = NULL; 
    psr_t psr;

    tmp = (void *)mem;//container_of(mem, struct mem, buffer);
    psr = ENTER_CRITICAL();
    list_add(&tmp->list, &mem_pool);
    //printf("free = 0x%p\n", &tmp->list);

    EXIT_CRITICAL(psr);
}

//初始化内存池
void mem_pool_init(){
     
    int i;
    psr_t psr;
    psr = ENTER_CRITICAL();
    for(i=0; i        list_add(&(gmem[i].list), &mem_pool);
        //printf("add mem 0x%p\n", &(gmem[i].list));
 }
 EXIT_CRITICAL(psr);
}

2.4 使用malloc和free一些建议

  • 不建议在中断中使用malloc。
  • 线程不一定安全,在-pthread进行编译是线程安全的,在freeRTOS的heap_3.c中进行封装pvPortMalloc是安全的,但是在其他环境要持怀疑态度。
  • malloc不一定会成功,需要check结果
  • malloc和free一定要成对出现。
  • free之后给指针加NULL,防止野指针。
  • 为了安全考虑,malloc之后的内存,需要memset置空后free掉,防止那块内存被分配可以读到数据。

3 Reference

Footnotes

  1. ARM Compiler armasm Reference Guide Version 6.00 - PUSH and POP

  2. arm-community-blogs - Using the Stack in AArch32 and AArch64

  3. Procedure Call Standard for the Arm® 64-bit Architecture (AArch64).pdf

  4. https://github.com/ARM-software/abi-aa

  5. 如何看待malloc产生内存碎片

  6. FreeRTOS kernel - Memory Management

  7. linux malloc free 内存碎片_嵌入式裸机编程中使用malloc、free会怎样?

04_ARMv8_指令集_运算指令集

04_ARMv8指令集-运算指令集

  • 加法指令ADD、ADDS、ADCS
  • 减法指令SUB、SUBS、SBC,SBCS,CMP
  • 位操作AND, ANDS, ORR、EOR、BFI、UBFX、SBFX

1. 加法指令

加法指令有ADD、ADDS、ADCS。 ADD一般性加法指令,ADCS带C标志位运算的加法指令,ADDS影响C标志位的加法运算。

1.1 ADD

a = a + b, 没有进位标志,也不会利用进位标志

  • ADD (extended register) :
    • Define: ADD <Xd|SP>, <Xn|SP>, <Wm>, {<extend> {#<amount>}}
    • Example1: add x0, x1, x2 ( x0 = x1 + x2 )
    • Example2: add x0, x1, x2, lsl #5( x0 = x1 + (x2 << 5) )
  • ADD (immediate):
    • Define: ADD <Xd|SP>, <Xn|SP>, #<imm>{, lsl <#shift>}, note shift supports #0 and #12 only.
    • Example1: add x1, x2, #8 (x1 = x2 + 8)
    • Example2: add x1, x2, #8, lsl #12 ( x1 = x2 + (8 << 12) )
  • ADD (shifted register):
    • Define: ADD <Xd>, <Xn>, <Xm>{, <shift> #<amount>} , note #amount range 0 to 63
    • Note: LSL when shift = 0, LSR when shift = 1, ASR when shift = 2
    • Example1: add x1, x2, x3, asr #2

1.2 ADDS

(a,C) = a + b, 带进位标志的加法,用法和ADD一样

1.3 ADCS

(a,C) = a + b + C,带进位标志的加法,且需要加上C标志位,用法和ADD一样。 注意,如果加法溢出的时候C标志位会置位为1,比如,a = 0xFFFFFFFFFFFFFFFF, b = 1,此时,加法溢出,C置位1。

1.4 ADR

a = b + PC, 当前程序的PC值加上给定的地址偏移

  • ADR
    • Define: ADR <Xd>, <label>
    • Note, no 32-bit
    • Note, range ±1MB, offset from the address of this instruction.
    • Example01: adr x1, #25

1.5 关于查看C flag的方法

方法1:使用MSR/MRS指令

	msr NZCV, xzr	   // clear the NZCV
	mrs x0, NZCV     // 查看NZCV寄存器,NZCV在高位28 - 32 bits

方法2:使用ADCS的+C特性

adcs x0, zxr, xzr 让两个0寄存器相加 0+0+c就可以得到C标志位的值

2. 减法指令

减法指令包含SBC,SBCS。请参考ARMv8手册,C6.2.231 C6-1299

2.1 SUB

a = a - b, 没有进位标志,也不会利用进位标志。使用方法和ADD一致。

2.2 SUBS

(a,N) = a - b, 会置标志位N。使用方法和SUBS一致,减成负数的时候,其余位置补1。

2.3 SBC

a = a - b - 1 + C

  • SBC (Subtract with Carry):
    • Define: SBC <Xd>, <Xn>, <Xm>
    • Example: sbc x0, xzr, xzr

2.4 SBCS

(a, N) = a - b - 1 + C, 如果减出负数的话,N会被置位

  • SBC (Subtract with Carry, setting N flag):
    • Define: SBCS <Xd>, <Xn>, <Xm>
    • Example: sbcs x0, xzr, x1

2.5 CMP

比较指令,实际上也使用SBC实现的, cmp x1, x2

  • 若x1 > x2, NCZV = 0100
  • 若x1 = x2, NCZV = 0110
  • 若x1 < x2, NCZV = 1000

Define 1: CMP <Xn|SP>, <R><m>{, <extend> {#<amount>}}

Define 2: CMP <Xn|SP>, #<imm>{, <shift>}

Define 3: CMP <Xn|SP>, <Xm>{, <shift> #<amount>}

Example:

** The function cmp_and_return_test:*

** if a >= b return 1*

** if a < b return 0*

test_cmp:
	cmp x0, x1   			// if x0 >= x1,  C is 1; if x0 < x1 C is 0
  adcs x0, xzr, xzr // 0 + 0 + C

3. 位操作

位操作包含AND, ANDS, ORR、EOR、BFI、UBFX、SBFX, 分别是与、与置位标志位、或、异或、插入、无符号提取、有符号提取。

3.1 ORR

a = a | b;

Define 1: ORR <Xd|SP>, <Xn>, #<imm>

Define 2: ORR <Xd|SP>, <Xn>, <Xm>{, <shift> #<amount>}

test_orr:
	// ORR test  0xAA oor 0x55 = 0xFF
	//           0xFF oor 0x00 = 0xFF
	//           0xFF oor 0xFF = 0xFF
	//           0x00 oor 0x00 = 0x00
	mov x0, xzr
	mov x1, #0xAA
	mov x2, #0x55
	orr x1, x1, x2

	mov x1, #0xFF
	orr x1, x1, xzr

	mov x1, #0xFF
	orr x1, x1, x1

	orr x1, xzr, xzr

	ret



test_ubfx:
	// x1: 0000 0000 0000 0000  ->  0000 0000 0000 1111
	//                     ^
	//                     |
	// x2:      0000 0000 1111 0000
	mov x1, xzr
	mov x2, #0x00F0
	ubfx x1, x2, #0x4, #0x4

	// x1: 0000 0000 0000 0000  ->  1111 1111 1111 1111
	//                     ^
	//                     |
	//          1000 0000 1111 0000
	mov x1, xzr
	mov x2, #0x80F0

3.2 EOR

a = a ^ b;

Define 1: EOR <Xd|SP>, <Xn>, #<imm>

Define 2: EOR <Xd|SP>, <Xn>, <Xm>{, <shift> #<amount>}

test_eor:
	// test 2 exchange the value x1 = 0x07, x2 = 0xAA
    // using the orr, just use two register.
	// x1 = x1^x2
	// x2 = x2^x1
	// x1 = x1^x2
	ldr x1, =0x07
	ldr x2, =0xAA
	eor x1, x1, x2
	eor x2, x2, x1
	eor x1, x1, x2
	ret

几个EOR的小技巧:

  • 翻转某些位: 比如把右数第0位到第3位翻转: 1010 1001 ^ 0000 1111 = 1010 0110
  • 交换数值: a=a^b; b=b^a; a=a^b,不借助第三个变量
  • 置0: a^a
  • 判断相等 a^b == 0

3.3 AND

3.3.1 AND

a = a & b;

Define 1: AND <Xd|SP>, <Xn>, #<imm>

Define 2: AND <Xd|SP>, <Xn>, <Xm>{, <shift> #<amount>}

	msr NZCV, xzr	   // clear the NZCV
	ldr x1, =0xAA
	ldr x2, =0x0
	// test AND, no Z flag. x1 = x1&x2
	and x1, x1, x2
	mrs x0, NZCV

3.3.2 ANDS

(a, z) = a & b. 如果a和b与的结果为0,z flag置位

	// test ANDS, z flag, if the result is 0, Z is 1
	msr NZCV, xzr	   // clear the NZCV
	mov x0, xzr
	ldr x1, =0xAA
	ands x1, x1, x2
	mrs x0, NZCV

3.4 BFI

Define 1: BFI <Xd>, <Xn>, #<lsb>, #<width>

从Xn寄存器里面从低位开始,插入到Xd寄存器从#开始,#长度。

读取Xn寄存器的低位开始计算,插入到Xd寄存器从#开始,#长度。这个没有办法控制Xd的位置,只能从Xd的最低位开始。

test_bfi:
	// 0000 0000 0000 1010
	//                 |
	//                 V
	//           0000 0000 0000 0000  ->  0000 1010 0000 0000
	ldr x1, =0x000A
	mov x2, xzr
	bfi x2, x1, #0x8, #0x4

	// 0000 0000 0000 1010
	//                 |
	//                 V
	//           0000 0101 0000 0000  ->  0000 1010 0000 0000
	ldr x1, =0x000A
	mov x2, #0x0500
	bfi x2, x1, #0x8, #0x4

	ret

3.5 UBFX/SBFX

Define: UBFX <Xd>, <Xn>, #<lsb>, #<width>

Define: SBFX <Xd>, <Xn>, #<lsb>, #<width>

读取Xn寄存器的#开始,#长度开始计算,替换到Xd寄存器低位的位置#长度。这个没有办法控制Xd的位置,只能从Xd的最低位开始。 SBFX是有符号的,替换之后Xd 其他位0变为1。

	// x1:      0000 0000 1111 0000
	//                     |
	//                     V
	// x2: 0000 0000 0000 0000  ->  0000 0000 0000 1111

	mov x1, #0x00F0
	mov x2, xzr
	ubfx x2, x1, #0x4, #0x4

	//          1000 0000 1111 0000
	//                     |
	//                     V
	// x1: 0000 0000 0000 0000  ->  1111 1111 1111 1111
	mov x1, #0x80F0
	mov x2, xzr
	sbfx x2, x1, #0x4, #0x4

image-20220319154625842

Ref

[1] Arm Armv8-A Architecture Registers-NZCV, Condition Flags

[2] ARM Cortex-A Series Programmer's Guide for ARMv8-A - Arithmetic and logical operations

[3] ARM架构(三)ARMv8 Programm Model Overview

[4] ARMv8官方手册学习笔记(三):寄存器

Linux进程之间的通信-消息队列(System V)

Linux进程之间的通信-消息队列(System V)

  • 信号量 (sem) : 管理资源的访问
  • 共享内存 (shm): 高效的数据分享
  • 消息队列 (msg):在进程之间简易的传数据的方法
  • 互斥算法(Dekker, Peterson, Filter, Szymanski, Lamport面包店算法)

1. 消息队列

1.1 消息队列和管道的区别

消息队列是System V的通信机制,排行老三,用于无关进程之间高效传递少量数据块。它和管道有一些类似,但是也有一些不一样1。这里列举一些消息队列和管道通信的不同点:

  • 管道是基于文件系统的,一个进程写,一个进程读,匿名管道是半双工的,限于亲缘进程;命名管道可以在无关进程之间通信,可以双向。

  • 消息通信方式以消息缓冲区为中间介质,通信双方的发送和接收操作均以消息为单位2。在存储器中,消息缓冲区被组织成队列,通常称之为消息队列。消息队列是随内核持续的,与有名管道(随进程持续)相比,生命力更强,应用空间更大。

  • 消息队列允许许多的消息排队,而每个信息可以有不同长度,而传统管道中的数据仅仅是一个数据流没有边界。

Difference between Pipes and Message Queues:3

S.NO Pipes Message Queues
1. Pipe is a form of Unix IPC that provides flow of data in one direction. Message queues is a form of system VIPC that store a linked list of messages
2. Creating a pipe using pipe() function, returns two file descriptors, one for reading another for writing. Creating a message queues using msgget() function returns a message queue identifier.
3. Pipes and FIFOs are unidirectional, i.e., the data can flow in one direction only. Message queues are bidirectional i.e., the data can flow in both directions.
4. With Pipes and FIFOs, the data must be fetched in first in first out order. With message queues the messages can be read in any order that is consistent with the values associ ated with the message types.
5. Priorities can’t be assigned to the messages. Priorities can assigned to the messages by associ ating a priority to a type or range of types.
6. With Pipes and FIFOs, there must be some process waiting for a message to be written over the pipes and FIFOs i.e., both a reader process and a writer must exist. With message queues a process can write the messages to a queue then exit, so that the messages can be read by another process at a later time.
7. Pipes are completely deleted from the system, when the last process having reference to it terminates. Message queue and its contents remain in the system on process termination until they are specifically read or deleted by some process calling mcgregor magento, by executing the ipcrm(1) command or by rebooting the system.
8. The maximum number of bytes that can be written to a pipe of FIFO is 4096 bytes. The maximum message size that can be written to a message queue is 8192 bytes.
9. A major advantage of using named pipes is that they provide a useful way to send one-line requests to an OpenEdge background session running a message handler procedure better Performance. Message queues enable asynchronous communication, which means that the endpoints that are producing and consuming messages interact with the queue, not each other.
10. Multiple users can send requests through the same named pipe and each request is removed from the pipe as it is received. Increased Reliability.

1.2 消息队列与内存共享

消息队列和内存共享都有互相传递消息的可能性,但这个话题主要是想讨论的是什么时候用内存共享合适,什么时候用消息队列合适。我们从文献4中可以得到很好的论证:

  • As understood, once the message is received by a process it would be no longer available for any other process. Whereas in shared memory, the data is available for multiple processes to access.
  • If we want to communicate with small message formats.
  • Shared memory data need to be protected with synchronization when multiple processes communicating at the same time.
  • Frequency of writing and reading using the shared memory is high, then it would be very complex to implement the functionality. Not worth with regard to utilization in this kind of cases.
  • What if all the processes do not need to access the shared memory but very few processes only need it, it would be better to implement with message queues.
  • If we want to communicate with different data packets, say process A is sending message type 1 to process B, message type 10 to process C, and message type 20 to process D. In this case, it is simplier to implement with message queues. To simplify the given message type as 1, 10, 20, it can be either 0 or +ve or –ve as discussed below.
  • Ofcourse, the order of message queue is FIFO (First In First Out). The first message inserted in the queue is the first one to be retrieved.

文献里面还提供了两个场景:

场景一:一对一的A->B的通信

message_queue

场景二:一对多的通信,可以指定优先级

multiple_message_queue

Step 1 − Create a message queue or connect to an already existing message queue (msgget())

Step 2 − Write into message queue (msgsnd())

Step 3 − Read from the message queue (msgrcv())

Step 4 − Perform control operations on the message queue (msgctl())

2. APIs

  • msgctl
  • msgget
  • msgrcv
  • msgsnd

2.1 msgget4

The msgget() system call returns the System V message queue identifier associated with the value of the key argument. It may be used either to obtain the identifier of a previously created message queue (when msgflg is zero and key does not have the value IPC_PRIVATE), or to create a new set.

A new message queue is created if key has the value IPC_PRIVATE or key isn't IPC_PRIVATE, no message queue with the given key key exists, and IPC_CREAT is specified in msgflg.

If msgflg specifies both IPC_CREAT and IPC_EXCL and a message queue already exists for key, then msgget() fails with errno set to EEXIST. (This is analogous to the effect of the combination O_CREAT | O_EXCL for open(2).)

#include <sys/msg.h>
int msgget(key_t key, int msgflg);

Parameters:

Params I/O Details
key_t key Input 提供一个key
int shmflg Input **IPC_CREAT **/IPC_EXCL /**SHM_HUGETLB **/SHM_HUGE_2MB /SHM_HUGE_1GB/ SHM_NORESERVE

Return:

  • > 0 成功
  • -1 失败

2.2 msgsnd/msgrcv5

The msgsnd() and msgrcv() system calls are used to send messages to, and receive messages from, a System V message queue. The calling process must have write permission on the message queue in order to send a message, and read permission to receive a message.

The msgp argument is a pointer to a caller-defined structure of the following general form:

struct msgbuf {
    long mtype;       /* message type, must be > 0 */
    char mtext[1];    /* message data */
};

The mtext field is an array (or other structure) whose size is specified by msgsz, a nonnegative integer value. Messages of zero length (i.e., no mtext field) are permitted. The mtype field must have a strictly positive integer value. This value can be used by the receiving process for message selection (see the description of msgrcv() below).

#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,
               int msgflg);

2.3 msgctl6

msgctl() performs the control operation specified by cmd on the System V message queue with identifier msqid.

The msqid_ds data structure is defined in <sys/msg.h> as follows:

struct msqid_ds {
    struct ipc_perm msg_perm;   /* Ownership and permissions */
    time_t          msg_stime;  /* Time of last msgsnd(2) */
    time_t          msg_rtime;  /* Time of last msgrcv(2) */
    time_t          msg_ctime;  /* Time of creation or last
                                   modification by msgctl() */
    unsigned long   msg_cbytes; /* # of bytes in queue */
    msgqnum_t       msg_qnum;   /* # number of messages in queue */
    msglen_t        msg_qbytes; /* Maximum # of bytes in queue */
    pid_t           msg_lspid;  /* PID of last msgsnd(2) */
    pid_t           msg_lrpid;  /* PID of last msgrcv(2) */
};

The fields of the msgid_ds structure are as follows:

#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);

对于command,依然有IPC_STAT/IPC_SET/IPC_RMID三个命令。

3. Example

创建两个程序,test_process_msg1.c用于接收消息,test_process_msg2.c用于发送消息。我们将允许两个程序都可以创建消息队列,但只有接受者在接受完最后一个消息之后可以删除他。

test_process_msg1.c: 用于接收消息:

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <sys/msg.h>

#define debug_log printf("%s:%d--", __FUNCTION__, __LINE__);printf

struct my_msg_st {
    long int my_msg_type;
    char some_text[BUFSIZ];
};

int main(int argc, char *argv[])
{
    int i, ret;
    char op_chars[20];
    int count = 0;
    int msg_id = 0;
    int running = 1;

    struct my_msg_st data;
    long int msg_to_recv = 0;

    debug_log("call the msgget function\n");
    msg_id = msgget((key_t) 1234, 0666 | IPC_CREAT);
    if (msg_id < 0) {
        debug_log("failed on semget\n");
        goto finish2;
    }
    while(running) {
        ret = msgrcv(msg_id, (void*)&data, BUFSIZ, msg_to_recv, 0);
        if (ret == -1) {
           debug_log("failed on msgrcv\n");
           goto finish2;
        }
        debug_log("You wrote: %s", data.some_text);
        if (strncmp(data.some_text, "end", 3) == 0) {
            running = 0;
        }
    }
    debug_log("finish.....\n");
finish2:
finish1:
    if (msgctl(msg_id, IPC_RMID, 0) == -1) {
        debug_log("failed on msgctl\n");
    }
    debug_log("finish test...\n");
    return ret;
}

test_process_msg2.c: 用于发送消息:

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <sys/msg.h>

#define debug_log printf("%s:%d--", __FUNCTION__, __LINE__);printf
#define MAX_TEXT 512
struct my_msg_st {
    long int my_msg_type;
    char some_text[MAX_TEXT];
};

int main(int argc, char *argv[])
{
    int i, ret;
    char op_chars[20];
    int count = 0;
    int msg_id = 0;
    int running = 1;
    char buffer[BUFSIZ];

    struct my_msg_st data;
    long int msg_to_recv = 0;

    debug_log("call the msgget function\n");
    msg_id = msgget((key_t) 1234, 0666 | IPC_CREAT);
    if (msg_id < 0) {
        debug_log("failed on semget\n");
        goto finish2;
    }
    while(running) {
        printf("Enter some text: ");
        fgets(buffer, BUFSIZ, stdin);
        data.my_msg_type = 1;
        strcpy(data.some_text, buffer);
        ret = msgsnd(msg_id, (void*)&data, MAX_TEXT, 0);
        if (ret == -1) {
           debug_log("failed on msgsnd\n");
           goto finish2;
        }
        if (strncmp(data.some_text, "end", 3) == 0) {
            running = 0;
        }
    }
    debug_log("finish.....\n");
finish2:
finish1:
    debug_log("finish test...\n");
    return ret;
}

image-20220402111227651

Ref

Footnotes

  1. 消息队列和管道的区别(转载)

  2. 管道和消息队列的区别

  3. Difference between Pipes and Message Queues

  4. The Usefulness of POSIX Message Queues 2

  5. Linux Programmer's Manual - msgget - get a System V message queue identifier

  6. Linux Programmer's Manual - msgctl - System V message control operations

08_ARMv8_链接器和链接脚本

08_ARMv8_链接器和链接脚本

我们在03_ELF文件_静态链接里面提到了对于一般性Linux的最终的ELF文件的内部结构,包含了各种段,然后相同的符号是如何进行链接的,怎么生成一个ELF文件的。在Linux里面,使用aarch64-linux-gnu-gcc可以说是一个比较high-level的编译器,里面有个默认的链接配置,而如果使用aarch64-none-elf-gcc这种baremental环境下的编译器,可能就要自己进行链接和写一些链接脚本。我们研究ARMv8的gcc、ld的编译算是对ELF_静态链接的一个扩展,来更熟悉链接这个过程,加深对于ELF文件的理解。

01 aarch64-none-elf-ld1

链接器英文是Linker,用于把多个目标文件的代码段、数据段、符号表等链接到一起组合成为一个二进制文件的bin-utils工具。为什么叫LD?根据文献2,LD的原名叫做,loaDer和Link eDitor,所以简写为LD。我们来研究一下ld的命令行:

1.1 cmd

ld -o output.bin /lib/crt0.o hello.o -lc

上面是把crt0.o和hello.o以及libc.a连接成output.bin,-lc的表示包含libc(这个我试了一下aarch64-编译器无论是linux的还是baremental的都并不支持这个选项,只有x86自身的gcc是支持的)

看一个比较复杂的例子,在benos里面的例子:

aarch64-linux-gnu-ld -T src/linker.ld -Map benos.map -o build/benos.elf build/printk_c.o build/irq_c.o build/string_c.o

这里就有几个比较常用的ld的command line的参数,ld的参数实在是太多了看着有点绝望,先暂时掌握以下几个,然后浏览一下文档大概有个印象:

选项 说明
-T 指令链接脚本
-Map 输出一个符号表
-o 输出的二进制文件
-b 执行目标代码输入文件的格式
-e 使用指定的符号作为程序的初试执行点 aarch64-none-elf-ld -o out.bin a.o b.o -e main
-l 执行库文件添加到要链接的文件清单中
-L 指定路径添加到搜索库的目录中
-S 忽略来自输出文件的调试器符号信息
-s 忽略来自输出文件的所有符号信息
-t 在处理输入文件时显示他们的名称
-Ttext 使用指定的地址作为代码段的起始点
-Tdata 使用指定的地址作为数据段的起始点
-Tbss 使用指定地址作为bss段的起始点
-Bstatic 只使用静态库
-Bdynamic 只是用动态库
-defsym 在输出文件中定义指定的全局符号

1.2 链接脚本

1.2.1 基础例子

-T参数指定ld的链接脚本,可以告诉链接器,最终生成的可执行文件需要按照工程师的意愿进行空间布局。

SECTIONS{
	. = 0x100000;				// . is location counter (lc)
	.text : { *(.text) }		// 所有的o文件的text段
	. = 0x800000;
	.data : { *(.data) }    // 所有o文件的data段
	.bss : { *(.bss) }
}

我们现在准备a.c和b.c文件,然后使用gcc -c的方式把两个文件编译成独立的.o文件,然后使用上面的链接脚本,把text段放在0x100000,并把data段放在0x800000。

$ aarch64-none-elf-ld -o out.bin a.o b.o -e main -T linker.ld

使用objdump来来查看elf文件结构

$ aarch64-none-elf-objdump -s -d out.bin

image-20220406095759022

观察发现,.text已经被映射到0x100000上面,.data已经被映射到0x800000上面。我们对linker的脚本稍微做下修改,让a.o和b.o的text段在不同的位置,a.o的text段在0x100000,而b.o的text段在0x20000,使用ld进行链接,然后查看readelf的结果

SECTIONS{
	. = 0x100000;
	.text : { a.o(.text) }
	. = 0x200000;
	.text : { b.o(.text) }
	. = 0x800000;
	.data : { *(.data) }
    . = 0x900000;
	.bss : { *(.bss) }
}

对于输入文件我们还可以使用EXCLUDE_FILE的内置函数来排除一些文件,例如EXCLUDE_FILE (*CRETN.o )

$ aarch64-none-elf-ld -o out.bin a.o b.o -e main -T linker.ld

不使用objdump,使用readelf也可以拿到这个值:

aarch64-none-elf-readelf -s out.bin

image-20220406100042857

看到两个.text段,一个是a.o的text,一个是b.o的text段。如果在linker脚本中不指定lc的位置,那么紧凑排列的

1.2.2 指定入口

上面所有命令都是-e main指定程序入口的地址,在链接脚本中也可以指定使用ENTRY()内置函数。

ENTRY(main)
SECTIONS{
    . = 0x100000;
    .text : { a.o(.text) }
    . = 0x200000;
    .text : { b.o(.text) }
    . = 0x800000;
    .data : { *(.data) }
    . = 0x900000;
    .bss : { *(.bss) }
}

优先级:

  • -e指定入口地址
  • ENTRY内置函数
  • 特定符号 start
  • 使用代码段的起始段
  • 使用地址0

1.2.3 符号赋值与引用

我们在linker脚本里面定义了start_of_text标识符,然后在c语言文件里面使用extern char start_of_text[];就可以轻松访问到这个位置的地址。

ENTRY(main)
SECTIONS{
    . = 0x100000;
    start_of_text = .;
    .text : { a.o(.text) }
    end_of_text = .;
    . = 0x200000;
    .text : { b.o(.text) }
    . = 0x800000;
    .data : { *(.data) }
    . = 0x900000;
    .bss : { *(.bss) }
}

甚至还可以在这里面 + - * /的计算地址。

1.2.4 section指定格式

section [address] [(type)]:
	[AT(lma)]
	[ALIGN(section_align)]
	[constraint]
	{
		output-section-command
		output-section-comaand
		...
	} [>region] [AT>lma_region] [:phdr :phdr ...] [=fillexp]
名字 含义
section 段的名字,例如.text .data段
address 虚拟地址的名字
type 输出端的属性
lma 加载地址
ALIGN 对齐要求
output-section-command 描述输入端如何映射到输出段
region 特定的内存区域
phdr 特定的程序段

Note, 如果没有AT指定LMA的地址,那么LMA和VMA是一致的。在嵌入式系统中,经常存在加载地址和虚拟地址不同的情况,如将映像文件加载到开发板的闪存中(由LMA指定),而bootloader需要将闪存内的文件复制到SDRAM里面(由VMA指定)。

我们这里根据benos里面提示的,构建一个基于ROM的映像文件,设定VMA和LMA地址不一样,映像文件存储在ROM中,运行程序的时候需要把ROM的映像文件复制到RAM中。ROM对应LMA,RAM对应VMA。

SECTIONS {
    .text   0x1000 :
    {
        *(.text);
        _etext = .;
    }

    .mdata  0x2000 :
    AT ( ADDR (.text) + SIZEOF (.text) )
    {
        _data = .;
        *(.data)
        _edata = .;
    }

    .bss    0x3000:
    {
        _bstart = .;
        *(.bss)
        *(COMMON)
        _bend = .;
    }
}

在链接脚本里面.mdata的区域为.text的基地址加上.text的大小,所以能看见VMA还是保持原来的样子,但是LMA已经变为0x1060了。

aarch64-none-elf-objdump -h out.bin

image-20220406112849492

这样我们需要把0x1060地址的内容加载到0x2000的位置。

extern char _etext, _data, _edata, _bstart, _bend;
char *src = &_etext;
char *dst = &_data;

while(dst < &_edata)
	*dst ++ = *src++;

1.2.5 内建函数

  • ABSOLUTE: 绝对值

    . = 0xb00000;
    my_offset1 = ABSOLUTE(0x100);				// 等于0x100
    my_offset2 = 0x100;									// 等于0xb00100
    
  • ADDR:返回虚拟地址

  • ALIGN: 声明下一个align对齐的地址

  • SIZEOF: 一个段的大小

02 重定位

现在就有个问题了,比如我们编译计算机的程序,如果elf文件都把运行地址定好了,那么就会存在一个问题,不同的机型有着不同的内存结构,而且不同的机器上面运行的app也不一样,我们elf文件中锁定的地址说不定已经被某个应用占用了,那么这个问题如何解决的呢?肯定有一个机制去管理这些地址,让这些地址可以根据自己机器的运行状况,使用内存大小做了一个相对化的处理。我们需要了解三个地址:

  • 加载地址:LMA,ARM64处理器上电复位后是从异常向量表开始读取第一条指令,所以通常这个地方是存放代码最开始的部分。
  • 运行地址:程序运行时的地址,可以叫做虚拟地址(线性地址)
  • 链接地址:在编译(汇编)之后,使用的相对地址,用于给链接器进行一些符号重定位和链接操作。LKA
  • 存储地址:存储地址和虚拟地址可能是等价的,如果开了MMU,程序在FLASH之类的这个地址和VMA会有不同。
  • 逻辑地址:CPU在没有开启重定位寄存器的时候,逻辑地址和物理地址一样。
  • 物理地址:也为内存地址寄存器地址。CPU单元看到的是逻辑地址,内存单元看到的是物理地址。

2.1 LMA=VMA=LKA

SECTIONS
{
	. = 0x80000,
	.text.boot : { *(.text.boot) }
	.text : { *(.text) }
	.rodata : { *(.rodata) }
	.data : { *(.data) }
	. = ALIGN(0x8);
	bss_begin = .;
	.bss : { *(.bss*) } 
	bss_end = .;
}

aarch64-none-elf-ld -o out.bin a.o b.o -T ben.ld -Map ben.map

输出ben.map查看地址映射:

$ cat ben.map

Memory Configuration

Name             Origin             Length             Attributes
*default*        0x0000000000000000 0xffffffffffffffff

Linker script and memory map

LOAD a.o
LOAD b.o
                0x0000000000080000                . = 0x80000

.text.boot
 *(.text.boot)

.text           0x0000000000080000       0x60
 *(.text)
 .text          0x0000000000080000       0x34 a.o
                0x0000000000080000                main
 .text          0x0000000000080034       0x2c b.o
                0x0000000000080034                b_func

.iplt           0x0000000000080060        0x0
 .iplt          0x0000000000080060        0x0 a.o

.rela.dyn       0x0000000000080060        0x0
 .rela.iplt     0x0000000000080060        0x0 a.o

.rodata
 *(.rodata)

.data           0x0000000000080060        0x4
 *(.data)
 .data          0x0000000000080060        0x0 a.o
 .data          0x0000000000080060        0x4 b.o
                0x0000000000080060                b_share

.igot.plt       0x0000000000080068        0x0
 .igot.plt      0x0000000000080068        0x0 a.o
                0x0000000000080068                . = ALIGN (0x8)
                0x0000000000080068                bss_begin = .

.bss            0x0000000000080068        0x8
 *(.bss*)
 .bss           0x0000000000080068        0x4 a.o
                0x0000000000080068                bsssss
 .bss           0x000000000008006c        0x4 b.o
                0x000000000008006c                bss_value
                0x0000000000080070                bss_end = .
OUTPUT(out.bin elf64-littleaarch64)
LOAD linker stubs

.comment        0x0000000000000000       0x57
 .comment       0x0000000000000000       0x57 a.o
                                         0x58 (size before relaxing)
 .comment       0x0000000000000057       0x58 b.o

2.2 LMA≠(LKA=VMA)

SECTIONS
{
	. = 0x80000,
   _stext_boot = .;
	.text.boot : { *(.text.boot) }
   _etext_boot = .;

	.text : AT(0x90000) { *(.text) }
   .rodata : { *(.rodata) }
	.data : { *(.data) }
	. = ALIGN(0x8);
	bss_begin = .;
	.bss : { *(.bss*) }
	bss_end = .;
}
Name             Origin             Length             Attributes
*default*        0x0000000000000000 0xffffffffffffffff

Linker script and memory map

LOAD a.o
LOAD b.o
                0x0000000000080000                . = 0x80000
                0x0000000000080000                _stext_boot = .

.text.boot
 *(.text.boot)
                0x0000000000080000                _etext_boot = .

.text           0x0000000000080000       0x60 load address 0x0000000000090000
 *(.text)
 .text          0x0000000000080000       0x34 a.o
                0x0000000000080000                main
 .text          0x0000000000080034       0x2c b.o
                0x0000000000080034                b_func

可以看到load地址已经和vma不一致了。load地址0x90000,而.text的VMA是0x80000,这种情况下如果程序想要启动,需要把代码段从加载地址复制到链接地址。

2.3 VMA≠LKA

我们实际的嵌入式设备有很多ROM和RAM的存储器,有这么多VMA、LMA、LKA3之类的也是因为有不同的加载stage导致的。我们理解,首先代码有编译阶段->存储到嵌入式NorFlash或者NandFlash阶段->装载DDR阶段->装载SRAM阶段->CPU执行阶段

  • 编译阶段
    • LMA和VMA不一致:存储到ROM阶段的时候需要拷贝ROM地址LMA的数据到VMA
    • 不存在链接地址的概念,或者说LMA = LKA
  • 存储到ROM阶段
    • 和编译阶段没有任何区别
    • ROM代码通常包含两部分,一部分是正常功能的,一部分是拷贝内存部分。
  • 装载SRAM阶段
    • 假设SRAM地址在(0x00)
    • 会把前4K的部分load到SRAM中。运行地址和链接地址不一样,因为SRAM在0x00
    • 指令集这面使用位置无关代码,都是根据PC值的相对位置寻址。
  • 装载DDR阶段
    • 假设DDR地址在(0x4000000)
    • CPU上电复位后从0x0取指令,运行的4K后面第部分 load到DDR
    • 等执行到DDR这面的程序之后,运行地址和链接地址就一致了
    • 这里使用位置有关代码和无关代码都可以。

B指令没办法实现在链接地址运行的程序,只能用LDR对PC寄存器修改值。后面MMU还会对这边进行比较深入的解析。

Ref

Footnotes

  1. GNU Development Tools - ld - The GNU linker

  2. Wikipedia - Linker (computing)

  3. 链接地址、运行地址、加载地址、存储地址

Qt_FFTW組件的編譯安裝

Qt上FFTW組件的編譯安裝

FFTW是一個做頻譜非常實用的組件,本文講述在Windows和Linux兩個平臺使用FFTW組件。Windows下的的FFTW組件已經編譯好成爲dll文件,按照開發應用的位數下載好組件包後直接按照dll規則使用組件;Linux下則需要自己進行編譯。

**FFTW源碼包的下載:**http://www.fftw.org/download.html

Linux編譯FFTW組件

1) 下載fftw-3.3.8.tar.gz文件,並解壓。

2) 配置fftw編譯選項

在終端輸入:

./configure --enable-type-prefix --prefix=/usr/local/fftw --with-gcc --disable-fortran --enable-i386-hacks  --enable-shared=yes

常見錯誤: 提示--enable-type-prefix沒有找到文件,此時請檢查上面命令每個選項之間的空格和縮進是否混淆,全部更改爲空格。

3) 編譯fftw

make -j8

4) 編譯安裝

make install

5) 編譯浮點fftw支持

make clean

./configure --enable-float --enable-type-prefix --prefix=/usr/local/fftw --with-gcc --disable-fortran --enable-i386-hacks --enable-shared=yes

6) 編譯fftw

make -j8

7) 編譯安裝

make install

最後在/usr/local/fftw路徑中又so、a文件。

Windows編譯FFTW組件

下载32位和64位版本后将该文件解压到自己想要设定的路径,我这里设定的c:/fftw。

然后,将文件中给所有的扩展名为.def 和 .dll文件拷贝到 qt安装路径\5.10.0\mingw53_32\bin中,(版本号可能有区别,但是大同小异)

如果不进行上述步骤,使用的fftw组件的应用程序编译是没有问题的,但是无法启动。当在调试模式下会提示,During startup program exited with code 0x00000135的错误(Qt的bug由第三方dll文件引起)。

在Qt安装路径\5.10.0\mingw53_32\include 路径中创建文件夹fftw,再将c:/fftw文件中的所有h文件,拷贝到该目录。

參考文獻:

[1] Installation and Customization,http://www.fftw.org/fftw2_doc/fftw_6.html#SEC69

ZYNQ的Linux Linaro系统镜像制作SD卡启动

ZYNQ的Linux Linaro系统镜像制作SD卡启动

0. 概述

ZYNQ生成uboot的时候和正常的ARM设备不太一样,ZYNQ属于二次辅助启动uboot然后由uboot启动内核,大概意思就是 ZYNQ内部有一个机制,该机制不可修改,可以通过拨码开关控制启动方式,比如从SD卡启动还是从QSPI启动,SD卡中要包含uboot的镜像信息。最大的不同就是,uboot编译完还不可以直接使用,还需要使用Vivado设计PL,再用SDK将uboot和设计PL的文件进行合成,最终合成后的文件拷贝到SD卡,由其启动。

我不会FPGA,本文也只概述在Linux端,SD卡如何做,如何制作一个全新的Linux系统。

映像文件BOOT.BIN一般包括:FSBL,Bitstream和SSBL这三个文件,其中Bitstream是配置PL端程序,是可选项,在我们制作Linaro系统的时候并不需要。FSBL是first stage boot loader,文件的制作需要使用Vivado环境;SSBL是Second Stage Boot Loader,这里使用的是Xilinx公司提供的u-boot。

来自参考文献1

1. 环境和材料

1.1 开发环境

  • 软件环境:Vivado 2017.02 Linux版本

  • 系统环境:Ubuntu 16.04 amd64

  • 交叉编译器: gcc-linaro-7.3-2018.05.tar.xz

    我的交叉编译环境放在/opt/toolschain/linaro/bin/arm-linux-gnueabihf-下,我编译的时候喜欢指定绝对编译器路径

1.2 准备材料

2. 制作uboot

2.1 编译uboot

  • 获取xilinx的uboot源码:git clone https://github.com/Xilinx/u-boot-xlnx.git

  • 清除编译:make CROSS_COMPILE=/opt/toolschain/linaro/bin/arm-linux-gnueabihf- ARCH=arm clean

  • 配置板级信息:make CROSS_COMPILE=/opt/toolschain/linaro/bin/arm-linux-gnueabihf- ARCH=arm zynq_zc702_defconfig 板级信息在如图所示位置,我的是zc701的板子,但是没有,我就选择一个和这个最相近的。

  • menuconfig写入配置信息:make CROSS_COMPILE=/opt/toolschain/linaro/bin/arm-linux-gnueabihf- ARCH=arm menuconfig

  • 编译uboot:make CROSS_COMPILE=/opt/toolschain/linaro/bin/arm-linux-gnueabihf- ARCH=arm -j8

  • 编译成功后生产的是uboot,所以需要重命名uboot: mv uboot uboot.elf

拿到uboot.elf后,留存备用,再合成最终的boot程序需要这个uboot.elf文件。

2.2 FSBL、bit文件的制作

大体流程就是:用Vivado这个软件新建工程,然后添加ip设计,配置时钟、配置一些Linux需要的基本外设(SD卡卡、串口、以太网等),使用wrap HDL功能生成顶层设计.v文件,然后编译.v文件生成.bit文件,再生成硬件描述文件,launch SDK软件,会自动生成一个工程,编译后拿到fsbl文件。

具体过程很多博友都已经写的很清楚了,我这里贴出一个讲的比较好的,可以按照这个方法做:在未来的多核通信机制里面,PS和PL的通信,则PL文件就是这样设计好之后然后我们重新合成uboot文件。

https://blog.csdn.net/long_fly/article/details/78643258

我们通过这样的方式拿到vivado编译生成的bit文件,并且在SDK里面建立了工程,生成了一个硬件平台,接下来我们获取fsbl这个文件。fsbl文件需要在SDK里面建立一个FSBL工程,并且基于刚才我们生成的硬件平台。

建立完之后直接编译,就可以拿到fsbl文件。

到目前位置,拿到了:

  • vivado编译生成的:bit文件
  • sdk生成的:fsbl文件
  • 刚刚编译uboot生成的:uboot.elf文件

可以开始合成BOOT.bin文件了

2.3 合成BOOT.bin

这个操作还是在sdk软件里面进行。

使用create boot image功能:

到此完成BOOT.bin的合成。

2.4 文件权限(仅限Linux开发用户)

还有一个非常重要的事情,我试了很多次,zynq平台就是不启动,uboot也不输出任何的信息。这个小小的问题卡了我很久,不过在今天早上洗漱的时候,突然想到,Xilinx Vivado和SDK都是在root情况下启动,生成BOOT.bin也是可能有权限问题。所以....

我拿到板子,然后在SD卡里面,给定sudo chmod 777 BOOT.bin 然后弹出SD卡,把SD放在ZYNQ上,居然成功启动了。如果你是在Linux系统下,不要忘记给定BOOT.bin权限。

3 Linux内核制作

3.1 Linux内核编译出uImage文件

  • 获取Linux内核:git clone https://github.com/Xilinx/linux-xlnx.git
  • 切换到Linux内核源码目录,开始清理内核:make CROSS_COMPILE=/opt/toolschain/linaro/bin/arm-linux-gnueabihf- ARCH=arm clean
  • 配置板级信息:make CROSS_COMPILE=/opt/toolschain/linaro/bin/arm-linux-gnueabihf- ARCH=arm xilinx_zynq_defconfig
  • 使用menuconfig写入.config文件:make CROSS_COMPILE=/opt/toolschain/linaro/bin/arm-linux-gnueabihf- ARCH=arm menuconfig 进来之后退出就行。
  • 编译内核:make CROSS_COMPILE=/opt/toolschain/linaro/bin/arm-linux-gnueabihf- ARCH=arm -j8
  • 制作uImage文件:make CROSS_COMPILE=/opt/toolschain/linaro/bin/arm-linux-gnueabihf- ARCH=arm uImage LOADADDR=0x00008000
  • 编译完成后,在linux-xlnx/arch/arm/boot的uImage文件留着备用。

3.2 制作设备树文件

在linux-xlnx/arch/arm/boot/dts目录内新建zynq-7010.dts文件,文件内容:

/dts-v1/;
/include/ "zynq-7000.dtsi"

/ {
    model = "HLF";
    compatible = "ALINX,zynq", "xlnx,zynq-7000";

    aliases {
        ethernet0 = &gem0;
        serial0 = &uart1;
        spi0 = &qspi;
        mmc0 = &sdhci0;
    };

    memory@0 {
        device_type = "memory";
        reg = <0x0 0x20000000>;
    };

    chosen {
        bootargs = "";
        stdout-path = "serial0:115200n8";
    };

    usb_phy0: phy0 {
        compatible = "usb-nop-xceiv";
        #phy-cells = <0>;
        reset-gpios = <&gpio0 46 1>;
    };
};

&clkc {
    ps-clk-frequency = <50000000>;
};

&gem0 {
    status = "okay";
    phy-mode = "rgmii-id";
    phy-handle = <&ethernet_phy>;

    ethernet_phy: ethernet-phy@0 {
        reg = <0>;
    };
};

&qspi {
    u-boot,dm-pre-reloc;
    status = "okay";
};

切换到内核的主目录里面:./scripts/dtc/dtc -I dts -O dtb -o ./arch/arm/boot/devicetree.dtb ./arch/arm/boot/dts/zynq-7010.dts

然后在linux-xlnx/arch/arm/boot/目录下即可发现devicetree.dtb文件,同样留着备用。

3.3 启动配置文件制作uEnv.txt

随便找个位置新建一个uEnv.txt 文件,文件内写入boot的配置信息:

uenvcmd=run linaro_sdboot

linaro_sdboot=echo Copying Linux from SD to RAM... && \
fatload mmc 0 0x3000000 ${kernel_image} && \
fatload mmc 0 0x2A00000 ${devicetree_image} && \
if fatload mmc 0 0x2000000 ${ramdisk_image}; \
then bootm 0x3000000 0x2000000 0x2A00000; \
else bootm 0x3000000 - 0x2A00000; fi

bootargs=console=ttyPS0,115200 root=/dev/mmcblk0p2 rw earlyprintk rootfstype=ext4 rootwait

保存,留着备用。

4 SD卡制作

准备一张空白的超过8G的SD卡,读卡器读取该卡,我们使用Linux系统进行格式化,Windows用户可以通过diskgen等格式化分区的软件制作也好。

  • 查看SD卡格式化分区:sudo fdisk -l 假如查看到的SD卡是/dev/sde分区,(不要格式化错了,在我年轻的时候我曾经把整个硬盘都格式化了,很危险的操作,看清楚是/dev/sd* 后面是c 还是d还是e还是f)。

  • 进入分区管理:sudo fdisk /dev/sde

  • 以下步骤按照这个OMAPL138制作SD卡启动盘及重装Linux系统,我的这个博客来。注意不同的是,我们建立启动分区的大小是100M即可,创建boot的分区的类型也为Linux。

  • 然后格式化boot分区:sudo mkfs.vfat -F 32 -n "boot" /dev/sde1

  • 格式化rootfs分区:sudo mkfs.ext4 -L "rootfs" /dev/sde2

    到此我们完成了SD卡制作。

5 烧写SD启动卡

  • sd卡的boot分区:使用命令将 BOOT.bin / devicetree.dtb / uImage / uEnv.txt 四个文件拷贝到boot分区。

  • 解压Linaro的文件系统: 在第一章写的 ARM端的Linaro文件系统:linaro-precise-ubuntu-desktop-20120723-305.tar.gz 解压到SD卡的root分区

    sudo tar --strip-components=3 -C /media/delvis/rootfs -xzpf linaro-precise-ubuntu-desktop-20120723-305.tar.gz binary/boot/filesystem.dir

  • 最好找一个带知识灯的读卡器,解压命令执行完了,不代表SD卡写入完毕,如果有指示灯,指示灯不闪烁之后弹出SD卡。

到此,一个完整的Linaro系统就写入了SD卡,将FPGA板子的boot拨码开关拨到SD卡启动位置,就可以看到Linaro系统启动了。

参考文献:

[1] long_fly, ZYNQ跑系统 系列(一) 传统方式移植linux, 2017年11月28日

[2] 雅可, Zedboard上运行Linaro系统(二):生成BOOT.BIN, 2016年07月26日

[3] 带你高飞, 03-ZYNQ学习(启动篇)之程序的固化, 2018年05月22日

06_Linux的动态共享库

06_Linux的动态共享库

这一部分很多都是常识性的知识,我们只整理一部分知识。

  • 共享库构造和析构函数
  • 清除符号信息
  • Linux系统符号版本机制

1 共享库构造和析构函数

Linux的共享库里面是有构造和析构函数的,构造函数和析构函数分别是在库的加载和销毁的时候被系统调用的,这里面分为两种情况:

  • 使用编译的链接器将动态链接库和目标文件链接
  • 使用dlopen在函数体内打开动态链接库

这两种情况的析构函数和构造函数调用的位置是不同的。第一种方法,在load二进制elf文件的时候,动态库被加载之后,就会调用构造函数,析构函数会在elf生命期结束之后调用。第二种方法,在dlopen的时候会调用构造函数,而dlclose的时候会调用析构函数。

构造函数使用方法:

void __attribute__((constructor(1))) init_function(void) {}

析构函数使用方法:

void __attribute__((destructor(5))) deinit_function(void) {}

在函数内实现这两个函数就可以了,函数名字可以自定,不可以少的是attribute,后面1和5数字代表着优先级。

1.1 使用编译链接法

我们准备一个lib文件:

#include <stdio.h>
#include "libprov.h"

#define debug_log printf("%s:%s:%d--",__FILE__, __FUNCTION__, __LINE__);printf

int prov_lib_init(const char *init_info)
{
    if (init_info == NULL) {
        debug_log("bad input parameter\n");
        return -1;
    }

    debug_log("do init: %s\n", init_info);

    return 0;
}

int prov_lib_do(const char *do_info)
{
    if (do_info == NULL) {
        debug_log("bad input parameter\n");
        return -1;
    }

    debug_log("do init: %s\n", do_info);

    return 0;
}

void prov_lib_cleanup()
{
    debug_log("do cleanup\n");
}

int __attribute__((constructor(5))) prov_lib_init_auto()
{
    debug_log("call the auto init\n");
    return 0;
}

void __attribute__((destructor(10))) prov_lib_cleanup_auto()
{
    debug_log("call the auto clean\n");
}

编译成动态库:gcc libprov.c -fPIC -shared -o libprov.so -g

准备一个user的C文件:

#include <stdio.h>
#include "libprov.h"

#define debug_log printf("%s:%s:%d--",__FILE__, __FUNCTION__, __LINE__);printf
C
int main(void)
{
    int ret = 0;
    debug_log("main start\n");
    ret = prov_lib_init("main init function\n");
    ret = prov_lib_do("main do prov\n");
    prov_lib_cleanup();
    debug_log("main end\n");
    return ret;
}

编译:gcc user_gcc_link.c libprov.so -o user.elf -g

image-20220415160441481

1.2 使用dlopen法

准备C文件:

#include <stdio.h>
#include <dlfcn.h>

#define debug_log printf("%s:%s:%d--",__FILE__, __FUNCTION__, __LINE__);printf

int main(void)
{
    int ret = 0;
    debug_log("main start\n");
    void *handle = NULL;
    int (*prov_init)(const char *) = NULL;
    int (*prov_do)(const char *) = NULL;
    void (*prov_cleanup)(void) = NULL;

    handle = dlopen("./libprov.so", RTLD_LAZY);
    if (!handle) {
        fprintf(stderr, "%s\n", dlerror());
        return -1;
    }
    prov_init = (int (*)(const char *)) dlsym(handle, "prov_lib_init");
    prov_do = (int (*)(const char *)) dlsym(handle, "prov_lib_do");
    prov_cleanup = (void (*)(void)) dlsym(handle, "prov_lib_cleanup");

    ret = prov_init("main init function\n");
    ret = prov_do("main do prov\n");
    prov_cleanup();

    dlclose(handle);

    debug_log("main end\n");
    return ret;
}

编译:gcc user_dl_open.c -o user.elf -g

image-20220415160717564

我们以后编写共享库的时候记得完善析构函数和构造函数。

2 清除符号信息

正常情况下编译出来的共享库和执行文件里面带有符号信息和调试信息,这些信息调试的时候非常有用,但是对于最终版来说是没有任何作用的,而且使得文件很大。使用strip的工具可以清除这些信息。

$ strip libprov.so

Note 在MACOX上使用strip 需要 -x1

或者在ld的时候使用-S参数(-s消除所有符号信息,-S消除调试信息)来表示清除调试信息。或者使用gcc中通过"-Wl,-s"和"-Wl,-S"来完成。

我们看看效果,使用objdump -t libprov.so打印所有符号

image-20220415162253254

3 Linux版本机制

在Linux下,我们使用ld链接共享库的时候,可以使用--version-script参数,控制版本。假设有个版本描述脚本叫做lib.ver。

gcc -shared -fPIC libprov.c -Xlinker --version-script lib.ver -o libprov.so

lib.ver脚本按照以下方式写:

VERS_1.2 {
		global:
			prov_lib_init;
			prov_lib_do;
			prov_lib_cleanup;
		local:
		    *;
};

这里面规定,这三个参数的版本是v1.2。此时我们编译,出来的函数是1.2版本的。如果们重新编译了低版本的库,这个时候会报错如图。

image-20220415164900139

综上,我们编写库的时候要注意:

  • 完善析构函数和构造函数。
  • release的时候注意strip文件。
  • 要定义好符号的版本号。

Ref

Footnotes

  1. Symbol stripping on OSX fails for release build

02_ARMv8_基本概念

Introduction

  • 新一代64位处理
  • 保持ARMv7兼容性

New feature

在programmer guide 2.1里面 引入那些feature:

  • Large physical address

    32位系统的没有enable的话,只支持4G。

  • 64bit virtual addressing

    使之虚拟地址空间可以超过4GB

  • automatic event sinaling

    支持原子操作的存储和访问的操作

  • larger register files

    减少对栈的使用,提高性能。

  • ...

  • Addtional 16KB 和64KB TLB

  • ...

  • Load-Acquire, Store Release instructions

  • NEON double-precision floating-points

ARMv8 some basic concepts

RM datasheet:

  • PE: Processing Element(处理机)

RISC架构的特性(RM提供的):

  • A large uniform register file. (ARMv7提供R0-R15,ARMv8提供更丰富的寄存器,比如X0-X30)

  • A load/store architecture, where data-processing operations only operate on register contents, not directly on memory contents.

  • Simple address modes, with all the load/store address determined from register contents and instruction fields only. (采用统一的简单的,比如内存映射MMU模式)

Execution States

The downward compatibility of ARMv7 shall be considered when the ARM designed the ARMv8 instruction set architecture,so the ARM designs the Execution State to be compatible with the ARMv7. There are the two types of `Execution State designed:

  • AArch64
  • AArch32

The two kinds of Execution State are like the different containers for different execution envs. In the lesson, the AArch64 execution state should be focused only. The difference between the AArch64 and the AArch32 exection states are showed as following table.

Features AArch64 AArch32
General-purpose registers 31-64bits (X30 is used as the procedure link register) 13-32bits registers
Program Counter (PC) one 64-bits registers one 32-bits register
Stack Pointers (SP) mulitple 64-bits registers one 32-bits register
Exception Link Register (ELR) mulitple 64-bits registers one 32-bits LR register use as ELR
Procedure Link Register one 64-bits, it is X30 register Sharing the LR with the ELR as PLR.
Advanced SIMD vector/floating-point 32 128-bits registers 32 64-bits registers
Instruction Set A64 A32 and T32
Excepiton model 4 excepiton levels, EL0-EL3 (ARMv8 Exception Model) PE modes and maps this onto the Armv8 Exception model (ARMv7 exception Model)
Virtual Addressing 64 bits virual address 32 bits virtual address
Process State (PSTATE) The A64 includes instructions that operate diectly on various PSTATE. Constract with the AArch64, the A32 and T32 operate them directly and can also use the APSR, CPSR to access.

Instruction Sets

arm的指令集有以下几种:

  • A64
  • A32/T32
AArch64 (A64)

Uses 32-bit instruction encodings and fixed-length.

AArch32 (A32/T32)

A32 uses 32-bit instruction encodings samely, and fixed-length. However, the T32 is a variable-length instruction set that uses both 16-bit and 32-bit instruction encodings.

Note, in AArch32, the A32/T32 instruction sets were called ARM and Thumb instruction sets. The ARMv8 instrcution set extends each of these instruction sets.

种类

https://github.com/carloscn/doclib/blob/master/man/arm/armv8/arm64_quick_reference.pdf

image-20220707170044598

image-20220707170110034

System Registers

  • Format
  • Types [D13 chapter in RM]

Supported Data Types

  • Byte 8bits
  • Halfword 16 bits
  • Word 32 bits
  • Doubleword 64 bits
  • Quadword 128 bits

Exception Levels (RM-D1.1)

  • EL0
  • EL1
  • EL2
  • EL3

image-20220210211952137

AArch64's Registers

  • R0-R30
  • SP
  • PC
  • V0-V31

image-20220630171323469

R0-R30

31 general-purpose registers. Each register can be accessed as X0-X30 (all the 64-bit are used) in 64 bit mode, W0-W30 (only low 32-bit are used). The Wx are Xx low 32-bit.

SP

64-bit dedicated Stack Pointer register.

PC

A 64-bit Program Counter holding the address of the current instruction. Software cannot write directly to the PC. It can only be updated on a branch, exception entry or exception return.

Processor State (RM-D1.7)

Condition Flags

  • N
  • Z
  • C
  • V

Exception Masking bits

  • D
  • A
  • I
  • F

Exection Status Control bits

  • SS
  • IL
  • nRW
  • EL
  • SP

Change log

  • [2022年6月29日]: 更新arm寄存器分类的脑图
  • [2022年7月7日]:
    • 增加对arm指令集分类的介绍
    • 指令分类图

Linux内核调用SPI驱动_实现OLED显示功能

Linux内核调用SPI驱动_实现OLED显示功能

0. 导语

进入Linux的世界,发现真的是无比的有趣,也发现搞Linux驱动从底层嵌入式搞起真的是很有益处。我们在单片机、DSP这些无操作系统的裸机中学习了这些最基本的驱动,然后用过GPIO时序去模拟、然后用那个芯片平台的外设去配置参数,到Linux的世界,对于底层的时序心中有数,做起来就容易很多。学习的过程就是不断的给自己出难题,然后去解决他,在未来工程里面遇到这个问题,就瞬间可以解决了,这就是经验的积累吧。

Linux驱动目录,包含了底层写好的SPI驱动,我们需要想办法调用人家写好的SPI驱动,就不需要写IO口模拟SPI时序了。在网络上,对于SPI应用级的驱动倒是很多,平台级驱动很少,而我们想把平台级驱动二次包装在我们的字符设备驱动中,对于用户,无需考虑SPI通信写协议还是写命令,只需要使用read和write函数写显示的内容就好了。

基于这样的想法,我们找了一个使用SPI协议的从器件来实现,我手里面有OLED设备,是支持SPI协议在OLED显示面板上显示字符的。所以搭建一个实验平台,做一个OLED的demo,未来所有的从SPI设备都遵循这个框架(而且我们在这个驱动中加入 了内核机制的驱动的自旋锁、互斥体的内核操作)。

实验平台如下:

  • ARM板子: 友善之臂Nano-T3 (CortexA53架构, Samsung s5c6818)
  • **ARM的Linux系统:**Ubuntu 16.04.2 LTS
  • **编译调试Linux:**Ubuntu 16.04.3 LTS amd64版本
  • **编译器:**arm-cortexa9-linux-gnueabihf-gcc (64位版本)
  • **从设备:**OLED (SPI模式)

1. 驱动架构模型

总体驱动架构模型如图所示,对于OLED驱动的表述,主要包含两个方面,一个是OLED这个传感器的抽象;一个是,misc字符驱动的注册,里面有read和write函数,供用户接口调用,(在read和write函数里面使用OLED设备表述里面的master控制oled的行为就好了,比如显示,清除,复位之类的)。

oled设备表述,为OLED设备的抽象,里面包含对硬件的描述和SPI的描述,还有对于写时序的时候使用自旋锁和互斥体对时序进行的保护,master为对oled设备的基本操作,包含复位,写字节等等。

在本博客中最重要的就是SPI平台驱动的使用,问题也非常的清晰,我们如何使用linux内核驱动里面写好的spi,参考Linux SPI API文档里面,那么复杂的结构体,哪些是在驱动中要使用,哪些是在应用级程序中使用的。网络上的资料大部分都是应用级的,没有讲述在字符驱动中二级注册spi驱动的,而我们对于OLED这样的SPI设备,则需要在驱动中调用,让用户无需关心任何SPI的调用。

在驱动模型中,master操作结构体里面,oled_write_byte这样的函数里面则需要调用系统级SPI,问题就非常明确,就在写byte的时候使用SPI。

那么我们就需要在注册完字符设备的时候,向内核注册spi,然后我们使用该SPI对OLED操作。

2. linux SPI驱动的注册

Linux Drivers目录具备一定的通用性也具备各个架构区别不同,在包含头文件的时候,要包含

  1. 通用性的linux spi文件 #include <linux/spi/spi.h>
  2. mach级特性文件#include <mach/slsi-spi.h>

同时也要关注:

  1. plat级的device.c文件,里面包含了spi_board信息的模板,用这个可以省去了很多麻烦。

我们使用的oled_hw_t, 图上的结构(OLED->hw)的具体定义,里面定义了io口的编号和spi的各种机制,注意谁是指针,谁是实体。

struct oled_hw_t 
{
	unsigned int res_io_num;
	unsigned int dc_io_num;
	struct spi_transfer		spi_trans;
	struct spi_message		spi_msg;
	struct spi_driver		*spi_drv;
	struct spi_device		*spi_dev;
	struct spi_master		*spi_master_bus;
};

我需要定义以下机制:

  • spi_driver

    spi_driver会向内核申请总线处理的权限,当我们加载驱动的时候,在ARM机器的linux上的/sys/bus/spi/drivers目录下会看到申请SPI驱动内核的名字。

    static const struct spi_device_id oled_spi_id[] =
    {
    		{“oledspi”, 1},
    		{},
    };
    static struct spi_driver sp6818_spi_driver = 
    {
    		.driver 			= 	
    		{
    				.name		=	"oled_spi",
    				.bus		=	&spi_bus_type,
    				.owner  	= 	THIS_MODULE,
    		},
    		.probe				=	oled_bus_spi_probe,
    		.remove 			= 	__devexit_p(oled_bus_spi_remove),
    		.suspend 			= 	oled_bus_spi_suspend,
    		.id_table			=	oled_spi_id,
    };
    MODULE_DEVICE_TABLE( spi, oled_spi_id );

    按照spi_driver驱动的格式进行,补充好probe和remove,suspend函数,但是这里存在一个问题,当我们应该spi_register_driver的时候,正常应该执行probe函数里面的内容,但是这个不执行,怀疑是因为二级包装问题,我们的主调还是使用misc驱动的字符设备 __init标示在 misc的初始化函数上,而导致不进入spi_driver的probe函数。

  • spi_device

    spi_device和spi_driver是成对出现的,在spi_driver注册完之后,则需要对spi_deivce进行配置,我们首先要声明一个spi_device,一会儿借助linux 的drivers 里面的platform级的deivce.c文件中的spi_board来注册我们的spi_device。

    定义spi_device驱动,这里面的配置信息可以瞎填,我们使用spi_board中的配置信息会覆盖这些信息。

    static struct spi_device sp6818_spi_device = 
    {
    		.mode				=	SPI_MODE_3,
    		.bits_per_word		=	16,
    		.chip_select		=	SPI_CS_HIGH,
    		.max_speed_hz		=	100000,
    };

    然后现在的工作就是如何spi_device和我们刚才spi_driver进行绑定了。

    定义下面的信息:

    static struct s3c64xx_spi_csinfo sp6818_csi = 
    {
            .line       		= 	OLED_CS_IO,
            .set_level  		= 	gpio_set_value,
            .fb_delay   		= 	0x2,
    };
    		
    struct spi_board_info sp6818_board_info = 
    {
            .modalias       	= 	"oled",
            .platform_data  	= 	NULL,
            .max_speed_hz   	= 	10 * 1000 * 1000,
            .bus_num        	= 	0,
            .chip_select    	= 	2,
            .mode           	= 	SPI_MODE_3,
            .controller_data    = 	&sp6818_csi,
    };

    这个模板就定义在platform级文件夹的device.c里面,我们按照模板的定义方式在我们的驱动文件里面也定义一个,在s3c64xx_spi_csinfo sp6818_csi中定义的是片选信号的IO口,这个IO口根据硬件原理图来的,然后定义spi_board_info结构体,这些都是为spi_device做准备的,spi的配置信息也由此写入。

    按照这个顺序进行:程序就如同下面的参考,后面会给出完成程序。

    static void oled_module_hw_init( OLED *self )
    {
    	int ret,i;
    	struct spi_master *master;
    	struct spi_device *spi;
    
    	self->hw.res_io_num = OLED_RES_IO;
    	self->hw.dc_io_num	= OLED_DC_IO;
    	printk( DRV_NAME "\tregister spi driver...\n" );
    	self->hw.spi_drv = &sp6818_spi_driver;
    	ret = spi_register_driver( self->hw.spi_drv );
    	if ( ret < 0 ) {
    		printk( DRV_NAME "\terror: spi driver register failed" );
    	}
    	printk( DRV_NAME "\tmaster blind spi bus.\n" );
    	master = spi_busnum_to_master( 0 );
    	master->num_chipselect = 4;
    	if ( !master ) {
    		printk( DRV_NAME "\terror: master blind spi bus.\n" );
    		ret = -ENODEV;
    		return ret;
    	}
    	printk( DRV_NAME "\tnew spi device...\n" );
    	spi =	spi_new_device( master, &sp6818_board_info );
    	if ( !spi ) {
    		printk( DRV_NAME "\terror: spi occupy.\n" );		
    		return -EBUSY;
    	}
    	self->hw.spi_master_bus	= master;
    	self->hw.spi_dev = spi;
    	printk( DRV_NAME "\thw init succussful...\n" );
    }

到此,完成,spi的注册。

spi_device的注册里面,会在ARM上面的Linux的/sys/bus/spi/devices下面出现我们注册的device设备,如图:

spi0.2就是我们所注册的device设备,这个命名就和我们的spi_board_info有关系了,

如果,bus_num = 5, chip_select = 20, 那么注册的device就是spi5.20了。这里还有个坑,就是片选信号的数值大小和master里面的片选num的问题,linux的spi api要求,master的num-chipselect必须大于 spi_board_info里面chip_select的数值。你也看到上面初始化程序,为什么master->num_chipselect = 4; 这个语句了

3. SPI 的使用

在驱动里面对于spi的使用就非常简单了。例如我们oled的write_byte函数:

static void oled_module_write_byte( OLED* self,				\ 
									unsigned int dat, 		\
									enum data_type_t type)
{
	int status;
	unsigned int write_buffer[1];

	if ( type == ENUM_WRITE_TYPE_CMD ) 
		self->master->set_dc_low( self );
	else 
		self->master->set_dc_high( self );
	write_buffer[0] = dat;
	write_buffer[1] = 0xFF;
	status = spi_write( self->hw.spi_dev, write_buffer, 1 );
	if ( status  )
		dev_err( &self->hw.spi_dev->dev, "%s error %d\n", __FUNCTION__, status );
}

使用spi_write函数就好了。

4. 结语

探索Linux SPI真是很费劲,这些花了好多时间,经历了无数次的实验,因为是驱动,经常在调试过程中出现暴栈、指针乱指,这些对于Linux内核都是毁灭性的错误,只能重启ARM Linux。光重启Linux就好几百次。不过总算是有成果,对于Linux驱动的学习还在进行,下次可能要实验I2C的平台驱动,找到规律和不同,再加上一些内核的操作,比如并发和IO等,在学习中成长。

源代码

Github地址:https://github.com/lifimlt/carlosdriver

见 oled.c oled.h 和oledfont.h三个文件

参考文献:

[1] Linux org, Serial Peripheral Interface (SPI),

[2] 郝过, Linux设备驱动模型SPI之二, 2016年2月28日

[3] invo-tronics , SPI Driver for Linux Based Embedded System, 2014年9月30日

[4] Linux学习之路, spi驱动框架全面分析,从master驱动到设备驱动, 2016年6月22日

05_ARMv8_指令集_跳转_比较与返回指令

05-ARMv8-指令集-跳转和比较指令

  • 零计数指令CLZ
  • 比较指令:CMP, CMN
  • 跳转指令:B, BR, BL, BLR
  • 条件选择指令:CSEL, CSET, CSINC

1. CLZ

计算最高为1的比特位前面有多少个0。例如, 0x0800 0000 0000 000F,前面是有4个0的(使用64位的寄存器),如果使用Wn寄存器,按照32位算。

  • CLZ
    • Define: CLZ <Xd>, <Xn>
    • Example1: clz x0, x1 [计算x1寄存器内的值最高位1的比特位前面有多少个0,并放入x0]

2. 比较指令

比较指令有CMP和CMN,CMP本质为:CMP x1, x2 -> x1 = x1 - x2;CMN为负向比较:CMN x1, x2->x1 = x1 + x2

2.1 CMP

NZC = a - b,根据不同的比较结果,来确定NZC的标志位,若x1 > x2, NCZV = 0100,若x1 = x2, NCZV = 0110,若x1 < x2, NCZV = 1000。

  • CMP (immediate):
    • Define: CMP <Xd|SP>, #<imm>{, lsl <#shift>}, note shift supports #0 and #12 only.
    • Example1: cmp x1, #8 (x1 = x2 - 8)
    • Example2: cmp x1, #8, lsl #12 ( x1 = x2 - (8 << 12) )
  • CMP (shifted register):
    • Define: CMP <Xd>, <Xm>{, <shift> #<amount>} , note #amount range 0 to 63
    • Note: LSL when shift = 0, LSR when shift = 1, ASR when shift = 2
    • Example1: cmp x1, x2, asr #2

2.1 CMN

NZC = a + b,a和b如果加和为负数,N置位; a + b的和为0,Z置位;a + b溢出(两个负数相加),C置位。

  • CMN (extended register) :
    • Define: CMN <Xd|SP>, <R><m>, {<extend> {#<amount>}}
    • Example1: cmn x0, x1 ( x0 = x0 - x1 )
    • Example2: cmn x0, x1, lsl #5( x0 = x0 - (x1 << 5) )
  • CMN (immediate):
    • Define: CMN <Xd|SP>, #<imm>{, lsl <#shift>}, note shift supports #0 and #12 only.
    • Example1: cmn x1, #8 (x1 = x2 - 8)
    • Example2: cmn x1, #8, lsl #12 ( x1 = x2 - (8 << 12) )
  • CMN (shifted register):
    • Define: CMN <Xd>, <Xm>{, <shift> #<amount>} , note #amount range 0 to 63
    • Note: LSL when shift = 0, LSR when shift = 1, ASR when shift = 2
    • Example1: cmn x1, x2, asr #2

对于CMN,根据实验有以下几种情况:

Condition Algo N Z C
-0x01 > -0x0E (-0x01) + (- 0x0E) = -0x0F 1 0 1
0 = 0 0 + 0 = 0 0 1 0
-0x0F < -0x01 (-0x0F) + (-0x01) = -0x10 1 0 1
0x0F > -0x01 (0x0F) + (-0x01) = 0x0E 0 0 1
-0x0F < 0x01 (-0x0F) + (0x01) = -0x0E 1 0 0
-0x01 < 0x01 (-0x01) + 0x01 = 0 0 1 1

2.3 Condition Codes

(CMP and CMN) Sets the condition flags to the result of a comparison if the original condition is true. If not true, the conditional flags are set to a specified condition flag state. The conditional compare instruction is very useful for expressing nested or compound comparisons.

Condition codes: 1

Code Encoding Meaning (when set by CMP) Meaning (when set by FCMP) Condition flags
EQ 0b0000 Equal to. Equal to. Z =1
NE 0b0001 Not equal to. Unordered, or not equal to. Z = 0
CS 0b0010 Carry set (identical to HS). Greater than, equal to, or unordered (identical to HS). C = 1
HS 0b0010 Greater than, equal to (unsigned) (identical to CS). Greater than, equal to, or unordered (identical to CS). C = 1
CC 0b0011 Carry clear (identical to LO). Less than (identical to LO). C = 0
LO 0b0011 Unsigned less than (identical to CC). Less than (identical to CC). C = 0
MI 0b0100 Minus, Negative. Less than. N = 1
PL 0b0101 Positive or zero. Greater than, equal to, or unordered. N = 0
VS 0b0110 Signed overflow. Unordered. (At least one argument was NaN). V = 1
VC 0b0111 No signed overflow. Not unordered. (No argument was NaN). V = 0
HI 0b1000 Greater than (unsigned). Greater than or unordered. (C = 1) && (Z = 0)
LS 0b1001 Less than or equal to (unsigned). Less than or equal to. (C = 0) || (Z = 1)
GE 0b1010 Greater than or equal to (signed). Greater than or equal to. N==V
LT 0b1011 Less than (signed). Less than or unordered. N!=V
GT 0b1100 Greater than (signed). Greater than. (Z==0) && (N==V)
LE 0b1101 Less than or equal to (signed). Less than, equal to or unordered. (Z==1) || (N!=V)
AL 0b1110 Always executed. Default. Always executed. Any
NV 0b1111 Always executed. Always executed. Any
// The test is:
// suppose the x1 = 1, x2 = -3
// Use the `cmn` instruction to compare the x1 and x2
// When the result is neg number, make x2 += 1;
// until the result is zero, then return the function.
test_cmn_jump:
	msr NZCV, xzr
	mov x0, xzr
	mov x1, #0x0
loop:
	add x1, x1, #0x1
	mov x2, #-0x3
	cmn x1, x2		// NZCV = 1000
	mrs x0, NZCV
	b.mi loop
	ret

3. 条件选择指令

条件选择指令包括CSEL\CSET\CSINC三条指令,也是非常重要的指令。

3.1 CSEL

CSEL, 条件选择指令,如果cond为真,那么xd就是xn的值,否则就是xm的值。注意,为上面表格的值。

  • Define: CSEL <Xd>, <Xn>, <Xm>, <cond>
  • Example1: csel x1, x1, x2, EQ -> 如果Z=1,x1维持原始值,否则x2给x1赋值
  • Example2: csel x1, x1, x2, MI -> 如果N=1,x1维持原始值,否则x2给x1赋值

3.2 CSET

CSET, 条件置位指令,如果cond为真,那么xd就是1,否则就是0。注意,为上面表格的值。

  • Define: CSET <Xd>, <cond>
  • Example1: cset x1, EQ -> 如果Z=1,x1是1, 否则为0
  • Example2: cset x1, MI -> 如果N=1,x1是1, 否则为0

3.3 CSINC

CSINC,条件选择并增加指令 如果cond为真,那么xd就是xn的值,否则就是xm+1的值。注意,为上面表格的值。

  • Define: CSINC <Xd>, <Xn>, <Xm>, <cond>
  • Example1: csinc x1, x1, x2, EQ -> 如果Z=1,x1维持x1, 否则为x2+1
  • Example2: csinc x1, x2, x3, MI -> 如果N=1,x1为x2, 否则为x3+1

3.4 Example

用汇编实现下面的c代码:

unsigned long cel_test(unsigned long a, unsigned long b)
{
  if (a == 0) {
    return b + 2;
  } else {
    return b - 1;
  }
}

分析拆解:

  • 比较指令 a与0 的比较,a为0的时候 Z标志位可以使用,因此可以利用Z标识位条件EQ作为该分支的入口。
  • b + 2和b-1 分支分别有个运算操作数的动作,确定 +2 还是 -1 可以把 2和 -1 放在一个寄存器里面,由 csel来判断那个值。
test_csel:

	mov x2, #0x2
	mov x3, #-0x1
	cmp x0, #0
	csel x4, x2, x3, EQ
	add x0, x1, x4
	ret

4. 跳转与返回指令

  • B: b跳转指令, b 可以跳到PC ±128MB的范围, 不返回
  • B. : 使用跳转指令b.<cond>,xx为以上表格的指,不返回
  • BR: 跳转到寄存器指定的地址处,不返回
  • BL:带返回地址的,PC±128MB, 用于跳转到子函数,返回地址为PC+4,设置到X30寄存器中。
  • BLX: 跳转到寄存器指定的地址处,可以返回。返回地址保存到X30寄存器,保存的是父函数的PC+4
  • RET: 从子函数返回,通常用X30里保存的返回地址返回。
  • ERET:从当前的异常模式返回,通常可以实现模式切换,例如EL1切换到EL0。它会从SPSR恢复PSTATE,从ELR中获取跳转地址,并返回到该地址。

Example:

  • 新建一个汇编文件
  • 创建一个bl_test的汇编函数,在该汇编函数中使用bl指令来跳转到csel_test汇编函数中
  • 在kernel.c文件中,C语言调用该bl_test汇编函数。

分析:

调用顺序应该是main -----> bl_test -----> csel_test ---> ret, 如果使用bl指令跳转到csel_test中,csel_test返回之后 bl_test拿到的是csel_test返回地址PC,而且X30寄存器被修改为csel_test子函数的地址,如果把PC+4返回给main函数,main函数看到的是当前的PC+4,肯定是错误的。这个难点就在于bl跳转子函数之后的返回需要处理好,重置X30寄存器的地址的值。

test_csel:

	mov x2, #0x2
	mov x3, #-0x1
	cmp x0, #0
	csel x4, x2, x3, EQ
	add x0, x1, x4
	ret

test_bl:
	mov x8, x30    // 备份main函数call进来x30的lr的值,否则到这里函数回不去
	mov x0, 1
	mov x1, 3
	bl test_csel   // 跳转进入这个函数之后x30寄存器被test_csel子函数冲走了
	mov x30, x8    // 恢复x30的值,此时ret之后可以回到main函数的地址继续执行
	retå

Ref

Footnotes

  1. ARM Cortex-A Series Programmer's Guide for ARMv8-A -Conditional instructions

01_ELF文件_目标文件格式

ELF文件---目标文件格式

1. 概述

有几个涉及的专有名词概念

  • 段(segment)/节(section)单位存储到elf文件中
  • 代码段(code section): .code 或者 .text
  • 数据段(data section): .data

这里面有几个一般性的规则:

  • .bbs段是存储全局变量和局部静态变量未初始化的。
  • .data段存储 已初始化全局变量和已初始化局部静态变量的位置。
  • .指令、函数调用、局部变量都存储在.text段(局部变量吃栈空间)
int a = 84;   // 已初始化全局变量 -> .data
int b;        // 未初始化全局变量 -> .bbs
int h[256];   // 未初始化全局变量 -> .bbs 并且h不占有真正的内存

void func_example (int i) {       // 指令函数地址 -> .text
	printf("example %d\n", i);
}

void main (void) {               // 指令函数地址 -> .text
    static int s_var_1 = 85;     // 已初始化静态变量 -> .data
    static int s_var_2;          // 未初始化静态变量 -> .bbs
    int c = 1;                   // 已初始化的非静态变量 -> .text
    int b;                       // 未初始化的非静态变量 -> .text
    func_example(s_var_1 + s_var_2 + c + b);   // 指令跳转 -> .text
    return;
}

在德州仪器的DSP系统中,他们制作的编译器使用cmd文件的方式来映射这些代码,本质原理是一样的。参考DSP-F2812的CMD文件

2. 目标文件生成及工具

2.1 C语言

研究编译文件,从一个最简单的mian.c文件开始,main.c文件可以表示为:

#include <stdio.h>

int a = 84;
int b;

void func(int i)
{
    printf("helloworld!%d\n", i);
}

int main(void)
{
    static int var_1 = 85;
    static int var_2;
    int c = 6;
    int d;
    func(var_1 + var_2 + c + d);
    return c;
}

// end of main.c

编译:$ aarch64-linux-gnu-gcc main.c -o a.out 生成a.out文件(ELF 64-bit executable, ARM aarch64)

2.2 段工具查看

使用objdump工具对查看elf文件内部结构 aarch64-linux-gnu-objdump -h a.out


a.out:     file format elf64-littleaarch64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .interp       0000001b  0000000000400200  0000000000400200  00000200  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  1 .note.ABI-tag 00000020  000000000040021c  000000000040021c  0000021c  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  2 .note.gnu.build-id 00000024  000000000040023c  000000000040023c  0000023c  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  3 .hash         00000028  0000000000400260  0000000000400260  00000260  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .dynsym       00000078  0000000000400288  0000000000400288  00000288  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  5 .dynstr       00000044  0000000000400300  0000000000400300  00000300  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  6 .gnu.version  0000000a  0000000000400344  0000000000400344  00000344  2**1
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  7 .gnu.version_r 00000020  0000000000400350  0000000000400350  00000350  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  8 .rela.dyn     00000018  0000000000400370  0000000000400370  00000370  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  9 .rela.plt     00000060  0000000000400388  0000000000400388  00000388  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 10 .init         00000014  00000000004003e8  00000000004003e8  000003e8  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 11 .plt          00000060  0000000000400400  0000000000400400  00000400  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 12 .text         000001f4  0000000000400460  0000000000400460  00000460  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 13 .fini         00000010  0000000000400654  0000000000400654  00000654  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 14 .rodata       00000027  0000000000400668  0000000000400668  00000668  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 15 .eh_frame     00000004  0000000000400690  0000000000400690  00000690  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 16 .init_array   00000008  0000000000410df8  0000000000410df8  00000df8  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 17 .fini_array   00000008  0000000000410e00  0000000000410e00  00000e00  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 18 .dynamic      000001d0  0000000000410e08  0000000000410e08  00000e08  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 19 .got          00000010  0000000000410fd8  0000000000410fd8  00000fd8  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 20 .got.plt      00000038  0000000000410fe8  0000000000410fe8  00000fe8  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 21 .data         00000018  0000000000411020  0000000000411020  00001020  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 22 .bss          00000010  0000000000411038  0000000000411038  00001038  2**2
                  ALLOC
 23 .comment      00000024  0000000000000000  0000000000000000  00001038  2**0
                  CONTENTS, READONLY
 24 .debug_aranges 00000110  0000000000000000  0000000000000000  00001060  2**4
                  CONTENTS, READONLY, DEBUGGING
 25 .debug_info   0000041d  0000000000000000  0000000000000000  00001170  2**0
                  CONTENTS, READONLY, DEBUGGING
 26 .debug_abbrev 0000018e  0000000000000000  0000000000000000  0000158d  2**0
                  CONTENTS, READONLY, DEBUGGING
 27 .debug_line   00000265  0000000000000000  0000000000000000  0000171b  2**0
                  CONTENTS, READONLY, DEBUGGING
 28 .debug_frame  00000068  0000000000000000  0000000000000000  00001980  2**3
                  CONTENTS, READONLY, DEBUGGING
 29 .debug_str    000002de  0000000000000000  0000000000000000  000019e8  2**0
                  CONTENTS, READONLY, DEBUGGING
 30 .debug_loc    00000166  0000000000000000  0000000000000000  00001cc6  2**0
                  CONTENTS, READONLY, DEBUGGING
 31 .debug_ranges 00000090  0000000000000000  0000000000000000  00001e30  2**4
                  CONTENTS, READONLY, DEBUGGING

2.3 关键字:

  • ALLOC – Section will have space allocated in the process when loaded. Set for all sections except those containing debug information.
  • LOAD – Section will be loaded from the file into the child process memory. Set for pre-initialized code and data, clear for .bss sections.
  • RELOC – Section needs to be relocated before loading.
  • READONLY – Section cannot be modified by the child process.
  • CODE – Section contains executable code only.
  • DATA – Section contains data only (no executable code).
  • ROM – Section will reside in ROM.
  • CONSTRUCTOR – Section contains data for constructor/destructor lists.
  • HAS_CONTENTS – Section is not empty.
  • NEVER_LOAD – An instruction to the linker to not output the section.
  • COFF_SHARED_LIBRARY – A notification to the linker that the section contains COFF shared library information.
  • IS_COMMON – Section contains common symbols.

还有个size工具可以直接看每个段的大小aarch64-linux-gnu-size a.out

$ aarch64-linux-gnu-size a.out
text    data     bss     dec     hex filename
1160     576      16    1752     6d8 a.out

2.4 代码段

2.4.1 指令段

objdump可以输出代码段aarch64-linux-gnu-objdump -s -d a.out 查看[附录一](#附录I:a.out objdump文件) 为文件全貌。前半部分为contents,后半部分为函数的汇编,这里拿C语言、Content、汇编进行对比:

C语言:

int main(void)
{
    static int var_1 = 85;
    static int var_2;
    int c = 6;
    int d;
    func(var_1 + var_2 + c + d);
    return c;
}

Content(由于main函数应该在content的.text段),截取text段为:

Contents of section .text:
 400460 1d0080d2 1e0080d2 e50300aa e10340f9  ..............@.
 400470 e2230091 e6030091 c0000058 e3000058  .#.........X...X
 400480 04010058 e7ffff97 eeffff97 00000000  ...X............
 400490 84054000 00000000 d0054000 00000000  ..@.......@.....
 4004a0 50064000 00000000 80000090 00f047f9  [email protected].
 4004b0 400000b4 dfffff17 c0035fd6 00000000  @........._.....
 4004c0 800000b0 00e00091 810000b0 21e00091  ............!...
 4004d0 3f0000eb a0000054 01000090 213843f9  ?......T....!8C.
 4004e0 410000b4 20001fd6 c0035fd6 1f2003d5  A... ....._.. ..
 4004f0 800000b0 00e00091 810000b0 21e00091  ............!...
 400500 210000cb 21fc4393 21fc418b 21fc4193  !...!.C.!.A.!.A.
 400510 a10000b4 02000090 423c43f9 420000b4  ........B<C.B...
 400520 40001fd6 c0035fd6 fd7bbea9 fd030091  @....._..{......
 400530 f30b00f9 930000b0 60e24039 80000035  ........`[email protected]
 400540 e0ffff97 20008052 60e20039 f30b40f9  .... ..R`..9..@.
 400550 fd7bc2a8 c0035fd6 e6ffff17 fd7bbea9  .{...._......{..
 400560 fd030091 a01f00b9 00000090 00001a91  ................
 400570 a11f40b9 b7ffff97 1f2003d5 fd7bc2a8  ..@...... ...{..
 400580 c0035fd6 fd7bbea9 fd030091 c0008052  .._..{.........R   <<<----main 函数指令 a9 fd 03 ...
 400590 a01f00b9 800000b0 00d00091 010040b9  ..............@.
 4005a0 800000b0 00f00091 000040b9 2100000b  ..........@.!...
 4005b0 a01f40b9 2100000b a01b40b9 2000000b  ..@.!.....@. ...
 4005c0 e7ffff97 a01f40b9 fd7bc2a8 c0035fd6  ......@..{...._.
 4005d0 fd7bbca9 fd030091 f4d701a9 94000090  .{..............
 4005e0 95000090 94023891 b5e23791 f6df02a9  ......8...7.....
 4005f0 940215cb f81f00f9 f603002a f70301aa  ...........*....
 400600 94fe4393 f80302aa 78ffff97 940100b4  ..C.....x.......
 400610 b30b00f9 130080d2 a37a73f8 e20318aa  .........zs.....
 400620 e10317aa e003162a 73060091 60003fd6  .......*s...`.?.
 400630 9f0213eb 21ffff54 b30b40f9 f4d741a9  [email protected].
 400640 f6df42a9 f81f40f9 fd7bc4a8 c0035fd6  ..B...@..{...._.
 400650 c0035fd6     

编译的汇编为:

0000000000400584 <main>:
  400584:	a9be7bfd 	stp	x29, x30, [sp, #-32]!
  400588:	910003fd 	mov	x29, sp
  40058c:	528000c0 	mov	w0, #0x6                   	// #6
  400590:	b9001fa0 	str	w0, [x29, #28]
  400594:	b0000080 	adrp	x0, 411000 <__libc_start_main@GLIBC_2.17>
  400598:	9100d000 	add	x0, x0, #0x34
  40059c:	b9400001 	ldr	w1, [x0]
  4005a0:	b0000080 	adrp	x0, 411000 <__libc_start_main@GLIBC_2.17>
  4005a4:	9100f000 	add	x0, x0, #0x3c
  4005a8:	b9400000 	ldr	w0, [x0]
  4005ac:	0b000021 	add	w1, w1, w0
  4005b0:	b9401fa0 	ldr	w0, [x29, #28]
  4005b4:	0b000021 	add	w1, w1, w0
  4005b8:	b9401ba0 	ldr	w0, [x29, #24]
  4005bc:	0b000020 	add	w0, w1, w0
  4005c0:	97ffffe7 	bl	40055c <func>
  4005c4:	b9401fa0 	ldr	w0, [x29, #28]
  4005c8:	a8c27bfd 	ldp	x29, 

可以看到十六进制 a9 -> stp, 91 -> mov

2.4.2 数据段

我们要找到a,b, var_1, var_2, c,d在代码段的位置

#include <stdio.h>

int a = 0x54;   // 已初始化全局变量 -> .data
int b;          // 未初始化全局变量 -> .bbs

void func(int i)
{
    ....
}

int main(void)
{
    static int var_1 = 0x55;   // 局部静态已初始化全局变量 -> .data
    static int var_2;          // 局部静态未初始化全局变量 -> .bbs
    int c = 6;                 // .text alloc
    int d;                     // .text alloc
    ....
}

// end of main.c
Contents of section .data:
 411020 00000000 00000000 00000000 00000000  ................
 411030 54000000 55000000                    T...U...        

从段中可以看出a (0x54)被映射到0x411030位置,var_1 (0x55)被映射到0x411034的位置。来看一下指令如何load这个地址的数据的。猜测指令应该为LDR x0, 4110300. -> STR x0。a变量没有被代码用到,在汇编指令里面找不到a地址操作的影子,但是var_1在main函数中进行了赋值,因此,可以找到:

0000000000400584 <main>:
  400584:	a9be7bfd 	stp	x29, x30, [sp, #-32]!
  400588:	910003fd 	mov	x29, sp
  40058c:	528000c0 	mov	w0, #0x6                   	// #6    <------- w0是0x6 局部变量c的位置
  400590:	b9001fa0 	str	w0, [x29, #28]
  400594:	b0000080 	adrp	x0, 411000 <__libc_start_main@GLIBC_2.17>
  400598:	9100d000 	add	x0, x0, #0x34
  40059c:	b9400001 	ldr	w1, [x0]                           <----- w1为变量d,加载的为x0地址内的值
  400594:	b0000080 	adrp	x0, 411000 <__libc_start_main@GLIBC_2.17>
  400598:	9100d000 	add	x0, x0, #0x34                      <---- x0基地址为411000然后加上0x34的偏移,得到 0x411034
  40059c:	b9400001 	ldr	w1, [x0]
  ....

2.4.3 String段

printf("%d....") 里面的固定字符串是放在了.rodata段,该段只读特性, const也会存入该段

Contents of section .rodata:
 400668 01000200 00000000 00000000 00000000  ................
 400678 00000000 00000000 68656c6c 6f776f72  ........hellowor
 400688 6c642125 640a00                      ld!%d..      

这个很明显了,放在地址,0x400680起始,可以找到指令段:

000000000040055c <func>:
  40055c:	a9be7bfd 	stp	x29, x30, [sp, #-32]!
  400560:	910003fd 	mov	x29, sp
  400564:	b9001fa0 	str	w0, [x29, #28]
  400568:	90000000 	adrp	x0, 400000 <_init-0x3e8>
  40056c:	911a0000 	add	x0, x0, #0x680                <- string的地址 0x400680被load进入x0寄存器
  400570:	b9401fa1 	ldr	w1, [x29, #28]
  400574:	97ffffb7 	bl	400450 <printf@plt>
  400578:	d503201f 	nop
  40057c:	a8c27bfd 	ldp	x29, x30, [sp], #32
  400580:	d65f03c0 	ret

2.4.4 BSS段

  • 符号表(Symbol Table)
  • static int x1 = 0; 即便是初始化,由于编译器的优化问题,也有可能会被放在.bss段.
  • aarch64-linux-gnu-objdump -s -d 不显示.bss段的内容.

2.5 自定义段

2.5.1 objcopy

把文件代码段化,使用aarch-linux-gnu-objcopy工具,例如把 objdump_h.txt文件代码段化:

aarch64-linux-gnu-objcopy -I binary -O elf64-littleaarch64 objdump_h.txt text.o

$ aarch64-linux-gnu-objdump -ht text.o

text.o:     file format elf64-little

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .data         000010be  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, DATA
SYMBOL TABLE:
0000000000000000 l    d  .data 0000000000000000 .data
0000000000000000 g       .data 0000000000000000 _binary_objdump_h_txt_start
00000000000010be g       .data 0000000000000000 _binary_objdump_h_txt_end
00000000000010be g       *ABS* 0000000000000000 _binary_objdump_h_txt_size

2.5.2 __attribute__自定义段

__attribute__((section("FOO"))) int global =4

__attribute__((section("BAR"))) void foo() {}

把global变量映射到CARLOS_DATA段,把func2映射到CARLOS_FUNC段中。

#include <stdio.h>

int a = 84;
int b;
const int g = 0xAA;
void func(int i)
{
    printf("helloworld!%d\n", i);
}

__attribute((section("CARLOS_DATA"))) int name = 4;
__attribute((section("CARLOS_FUNC"))) int func2 (void){
    int m = 9, n = 10;
    int q;
    q = m+n;
    return q;
}

int main(void)
{
    static int var_1 = 85;
    static int var_2;
    int c = 6;
    int d;
    func(var_1 + var_2 + c + d);
    return c;
}

编译 -> 使用aarch64-linux-gnu-objdump -h main 查看

13 CARLOS_FUNC   00000030  0000000000400654  0000000000400654  00000654  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 14 .fini         00000010  0000000000400684  0000000000400684  00000684  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 15 .rodata       0000002f  0000000000400698  0000000000400698  00000698  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 16 .eh_frame     00000004  00000000004006c8  00000000004006c8  000006c8  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 17 .init_array   00000008  0000000000410df8  0000000000410df8  00000df8  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 18 .fini_array   00000008  0000000000410e00  0000000000410e00  00000e00  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 19 .dynamic      000001d0  0000000000410e08  0000000000410e08  00000e08  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 20 .got          00000010  0000000000410fd8  0000000000410fd8  00000fd8  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 21 .got.plt      00000038  0000000000410fe8  0000000000410fe8  00000fe8  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 22 .data         00000018  0000000000411020  0000000000411020  00001020  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 23 CARLOS_DATA   00000004  0000000000411038  0000000000411038  00001038  2**2
                  CONTENTS, ALLOC, LOAD, DATA
 24 .bss          0000000c  000000000041103c  000000000041103c  0000103c  2**2
                  ALLOC
 25 .comment      00000024  0000000000000000  0000000000000000  0000103c  2**0
                  CONTENTS, READONLY

第13 line 和23 line 分别为我们自己映射的区域。

附录I:a.out objdump文件


a.out:     file format elf64-littleaarch64

Contents of section .interp:
 400200 2f6c6962 2f6c642d 6c696e75 782d6161  /lib/ld-linux-aa
 400210 72636836 342e736f 2e3100             rch64.so.1.     
Contents of section .note.ABI-tag:
 40021c 04000000 10000000 01000000 474e5500  ............GNU.
 40022c 00000000 03000000 07000000 00000000  ................
Contents of section .note.gnu.build-id:
 40023c 04000000 14000000 03000000 474e5500  ............GNU.
 40024c b5345575 e47d2302 2f0a0c94 37de1666  .4Uu.}#./...7..f
 40025c a10ff265                             ...e            
Contents of section .hash:
 400260 03000000 05000000 02000000 01000000  ................
 400270 04000000 00000000 00000000 00000000  ................
 400280 00000000 03000000                    ........        
Contents of section .dynsym:
 400288 00000000 00000000 00000000 00000000  ................
 400298 00000000 00000000 18000000 12000000  ................
 4002a8 00000000 00000000 00000000 00000000  ................
 4002b8 2a000000 20000000 00000000 00000000  *... ...........
 4002c8 00000000 00000000 0b000000 12000000  ................
 4002d8 00000000 00000000 00000000 00000000  ................
 4002e8 11000000 12000000 00000000 00000000  ................
 4002f8 00000000 00000000                    ........        
Contents of section .dynstr:
 400300 006c6962 632e736f 2e360061 626f7274  .libc.so.6.abort
 400310 00707269 6e746600 5f5f6c69 62635f73  .printf.__libc_s
 400320 74617274 5f6d6169 6e005f5f 676d6f6e  tart_main.__gmon
 400330 5f737461 72745f5f 00474c49 42435f32  _start__.GLIBC_2
 400340 2e313700                             .17.            
Contents of section .gnu.version:
 400344 00000200 00000200 0200               ..........      
Contents of section .gnu.version_r:
 400350 01000100 01000000 10000000 00000000  ................
 400360 97919606 00000200 39000000 00000000  ........9.......
Contents of section .rela.dyn:
 400370 e00f4100 00000000 01040000 02000000  ..A.............
 400380 00000000 00000000                    ........        
Contents of section .rela.plt:
 400388 00104100 00000000 02040000 01000000  ..A.............
 400398 00000000 00000000 08104100 00000000  ..........A.....
 4003a8 02040000 02000000 00000000 00000000  ................
 4003b8 10104100 00000000 02040000 03000000  ..A.............
 4003c8 00000000 00000000 18104100 00000000  ..........A.....
 4003d8 02040000 04000000 00000000 00000000  ................
Contents of section .init:
 4003e8 fd7bbfa9 fd030091 2e000094 fd7bc1a8  .{...........{..
 4003f8 c0035fd6                             .._.            
Contents of section .plt:
 400400 f07bbfa9 90000090 11fe47f9 10e23f91  .{........G...?.
 400410 20021fd6 1f2003d5 1f2003d5 1f2003d5   .... ... ... ..
 400420 900000b0 110240f9 10020091 20021fd6  ......@..... ...
 400430 900000b0 110640f9 10220091 20021fd6  ......@..".. ...
 400440 900000b0 110a40f9 10420091 20021fd6  [email protected].. ...
 400450 900000b0 110e40f9 10620091 20021fd6  [email protected].. ...
Contents of section .text:
 400460 1d0080d2 1e0080d2 e50300aa e10340f9  ..............@.
 400470 e2230091 e6030091 c0000058 e3000058  .#.........X...X
 400480 04010058 e7ffff97 eeffff97 00000000  ...X............
 400490 84054000 00000000 d0054000 00000000  ..@.......@.....
 4004a0 50064000 00000000 80000090 00f047f9  [email protected].
 4004b0 400000b4 dfffff17 c0035fd6 00000000  @........._.....
 4004c0 800000b0 00e00091 810000b0 21e00091  ............!...
 4004d0 3f0000eb a0000054 01000090 213843f9  ?......T....!8C.
 4004e0 410000b4 20001fd6 c0035fd6 1f2003d5  A... ....._.. ..
 4004f0 800000b0 00e00091 810000b0 21e00091  ............!...
 400500 210000cb 21fc4393 21fc418b 21fc4193  !...!.C.!.A.!.A.
 400510 a10000b4 02000090 423c43f9 420000b4  ........B<C.B...
 400520 40001fd6 c0035fd6 fd7bbea9 fd030091  @....._..{......
 400530 f30b00f9 930000b0 60e24039 80000035  ........`[email protected]
 400540 e0ffff97 20008052 60e20039 f30b40f9  .... ..R`..9..@.
 400550 fd7bc2a8 c0035fd6 e6ffff17 fd7bbea9  .{...._......{..
 400560 fd030091 a01f00b9 00000090 00001a91  ................
 400570 a11f40b9 b7ffff97 1f2003d5 fd7bc2a8  ..@...... ...{..
 400580 c0035fd6 fd7bbea9 fd030091 c0008052  .._..{.........R
 400590 a01f00b9 800000b0 00d00091 010040b9  ..............@.
 4005a0 800000b0 00f00091 000040b9 2100000b  ..........@.!...
 4005b0 a01f40b9 2100000b a01b40b9 2000000b  ..@.!.....@. ...
 4005c0 e7ffff97 a01f40b9 fd7bc2a8 c0035fd6  ......@..{...._.
 4005d0 fd7bbca9 fd030091 f4d701a9 94000090  .{..............
 4005e0 95000090 94023891 b5e23791 f6df02a9  ......8...7.....
 4005f0 940215cb f81f00f9 f603002a f70301aa  ...........*....
 400600 94fe4393 f80302aa 78ffff97 940100b4  ..C.....x.......
 400610 b30b00f9 130080d2 a37a73f8 e20318aa  .........zs.....
 400620 e10317aa e003162a 73060091 60003fd6  .......*s...`.?.
 400630 9f0213eb 21ffff54 b30b40f9 f4d741a9  [email protected].
 400640 f6df42a9 f81f40f9 fd7bc4a8 c0035fd6  ..B...@..{...._.
 400650 c0035fd6                             .._.            
Contents of section .fini:
 400654 fd7bbfa9 fd030091 fd7bc1a8 c0035fd6  .{.......{...._.
Contents of section .rodata:
 400668 01000200 00000000 00000000 00000000  ................
 400678 00000000 00000000 68656c6c 6f776f72  ........hellowor
 400688 6c642125 640a00                      ld!%d..         
Contents of section .eh_frame:
 400690 00000000                             ....            
Contents of section .init_array:
 410df8 58054000 00000000                    X.@.....        
Contents of section .fini_array:
 410e00 28054000 00000000                    (.@.....        
Contents of section .dynamic:
 410e08 01000000 00000000 01000000 00000000  ................
 410e18 0c000000 00000000 e8034000 00000000  ..........@.....
 410e28 0d000000 00000000 54064000 00000000  ........T.@.....
 410e38 19000000 00000000 f80d4100 00000000  ..........A.....
 410e48 1b000000 00000000 08000000 00000000  ................
 410e58 1a000000 00000000 000e4100 00000000  ..........A.....
 410e68 1c000000 00000000 08000000 00000000  ................
 410e78 04000000 00000000 60024000 00000000  ........`.@.....
 410e88 05000000 00000000 00034000 00000000  ..........@.....
 410e98 06000000 00000000 88024000 00000000  ..........@.....
 410ea8 0a000000 00000000 44000000 00000000  ........D.......
 410eb8 0b000000 00000000 18000000 00000000  ................
 410ec8 15000000 00000000 00000000 00000000  ................
 410ed8 03000000 00000000 e80f4100 00000000  ..........A.....
 410ee8 02000000 00000000 60000000 00000000  ........`.......
 410ef8 14000000 00000000 07000000 00000000  ................
 410f08 17000000 00000000 88034000 00000000  ..........@.....
 410f18 07000000 00000000 70034000 00000000  ........p.@.....
 410f28 08000000 00000000 18000000 00000000  ................
 410f38 09000000 00000000 18000000 00000000  ................
 410f48 feffff6f 00000000 50034000 00000000  ...o....P.@.....
 410f58 ffffff6f 00000000 01000000 00000000  ...o............
 410f68 f0ffff6f 00000000 44034000 00000000  ...o....D.@.....
 410f78 00000000 00000000 00000000 00000000  ................
 410f88 00000000 00000000 00000000 00000000  ................
 410f98 00000000 00000000 00000000 00000000  ................
 410fa8 00000000 00000000 00000000 00000000  ................
 410fb8 00000000 00000000 00000000 00000000  ................
 410fc8 00000000 00000000 00000000 00000000  ................
Contents of section .got:
 410fd8 080e4100 00000000 00000000 00000000  ..A.............
Contents of section .got.plt:
 410fe8 00000000 00000000 00000000 00000000  ................
 410ff8 00000000 00000000 00044000 00000000  ..........@.....
 411008 00044000 00000000 00044000 00000000  ..@.......@.....
 411018 00044000 00000000                    ..@.....        
Contents of section .data:
 411020 00000000 00000000 00000000 00000000  ................
 411030 54000000 55000000                    T...U...        
Contents of section .comment:
 0000 4743433a 20284c69 6e61726f 20474343  GCC: (Linaro GCC
 0010 20372e35 2d323031 392e3132 2920372e   7.5-2019.12) 7.
 0020 352e3000                             5.0.            
Contents of section .debug_aranges:
 0000 2c000000 02000000 00000800 00000000  ,...............
 0010 60044000 00000000 48000000 00000000  `[email protected].......
 0020 00000000 00000000 00000000 00000000  ................
 0030 1c000000 0200ab00 00000800 00000000  ................
 0040 00000000 00000000 00000000 00000000  ................
 0050 4c000000 02002201 00000800 00000000  L.....".........
 0060 a8044000 00000000 14000000 00000000  ..@.............
 0070 e8034000 00000000 0c000000 00000000  ..@.............
 0080 54064000 00000000 08000000 00000000  T.@.............
 0090 00000000 00000000 00000000 00000000  ................
 00a0 2c000000 0200c001 00000800 00000000  ,...............
 00b0 d0054000 00000000 84000000 00000000  ..@.............
 00c0 00000000 00000000 00000000 00000000  ................
 00d0 3c000000 02007f03 00000800 00000000  <...............
 00e0 f4034000 00000000 08000000 00000000  ..@.............
 00f0 5c064000 00000000 08000000 00000000  \.@.............
 0100 00000000 00000000 00000000 00000000  ................
Contents of section .debug_info:
 0000 a7000000 02000000 00000801 00000000  ................
 0010 60044000 00000000 a8044000 00000000  `.@.......@.....
 0020 2e2e2f73 79736465 70732f61 61726368  ../sysdeps/aarch
 0030 36342f73 74617274 2e53002f 686f6d65  64/start.S./home
 0040 2f746377 672d6275 696c6473 6c617665  /tcwg-buildslave
 0050 2f776f72 6b737061 63652f74 6377672d  /workspace/tcwg-
 0060 6d616b65 2d72656c 65617365 5f302f73  make-release_0/s
 0070 6e617073 686f7473 2f676c69 62632e67  napshots/glibc.g
 0080 69747e72 656c6561 73657e32 2e32357e  it~release~2.25~
 0090 6d617374 65722f63 73750047 4e552041  master/csu.GNU A
 00a0 5320322e 32382e32 00018073 00000004  S 2.28.2...s....
 00b0 00140000 0008014c 0000000c 50020000  .......L....P...
 00c0 f9000000 5a000000 0201083e 00000002  ....Z......>....
 00d0 02071200 00000204 07050000 00020807  ................
 00e0 00000000 02010640 00000002 02052500  .......@......%.
 00f0 00000304 05696e74 00044700 00000208  .....int..G.....
 0100 055e0100 00020108 47000000 052f0000  .^......G..../..
 0110 0001184e 00000009 03680640 00000000  ...N.....h.@....
 0120 00009a00 00000200 52000000 08018100  ........R.......
 0130 00000000 00002e2e 2f737973 64657073  ......../sysdeps
 0140 2f616172 63683634 2f637274 692e5300  /aarch64/crti.S.
 0150 2f686f6d 652f7463 77672d62 75696c64  /home/tcwg-build
 0160 736c6176 652f776f 726b7370 6163652f  slave/workspace/
 0170 74637767 2d6d616b 652d7265 6c656173  tcwg-make-releas
 0180 655f302f 736e6170 73686f74 732f676c  e_0/snapshots/gl
 0190 6962632e 6769747e 72656c65 6173657e  ibc.git~release~
 01a0 322e3235 7e6d6173 7465722f 63737500  2.25~master/csu.
 01b0 474e5520 41532032 2e32382e 32000180  GNU AS 2.28.2...
 01c0 bb010000 04006400 00000801 81010000  ......d.........
 01d0 0c4c0200 00f90000 00d00540 00000000  .L.........@....
 01e0 00840000 00000000 00fd0000 00020805  ................
 01f0 5e010000 037a0100 0002d844 00000004  ^....z.....D....
 0200 34000000 02080700 00000002 04070500  4...............
 0210 00000208 05590100 00021004 ad020000  .....Y..........
 0220 056b0000 006b0000 00060007 08710000  .k...k.......q..
 0230 00088600 00000986 00000009 8d000000  ................
 0240 098d0000 00000a04 05696e74 00070893  .........int....
 0250 00000007 08990000 00020108 47000000  ............G...
 0260 0b780200 00012860 0000000b b9020000  .x....(`........
 0270 012a6000 00000b67 01000001 2c600000  .*`....g....,`..
 0280 000bcd02 0000012e 60000000 05d70000  ........`.......
 0290 00d70000 00060007 08dd0000 000c0b39  ...............9
 02a0 02000001 30cc0000 000b6702 00000131  ....0.....g....1
 02b0 cc000000 0d570200 00015f50 06400000  .....W...._P.@..
 02c0 00000004 00000000 00000001 9c0e9802  ................
 02d0 00000143 d0054000 00000000 80000000  ...C..@.........
 02e0 00000000 019cb301 00000f8e 02000001  ................
 02f0 43860000 00000000 000fa802 00000143  C..............C
 0300 8d000000 4c000000 0f340200 0001438d  ....L....4....C.
 0310 00000098 00000010 93020000 01563f00  .............V?.
 0320 0000e400 0000110c 06400000 00000030  [email protected]
 0330 00000000 000000a5 01000012 69000157  ............i..W
 0340 34000000 07010000 13300640 00000000  4........0.@....
 0350 00140150 02860014 01510287 00140152  ...P.....Q.....R
 0360 02880000 00150c06 40000000 0000b301  ........@.......
 0370 00000016 a2020000 a2020000 0137009a  .............7..
 0380 00000002 007c0100 00080103 02000050  .....|.........P
 0390 0000002e 2e2f7379 73646570 732f6161  ...../sysdeps/aa
 03a0 72636836 342f6372 746e2e53 002f686f  rch64/crtn.S./ho
 03b0 6d652f74 6377672d 6275696c 64736c61  me/tcwg-buildsla
 03c0 76652f77 6f726b73 70616365 2f746377  ve/workspace/tcw
 03d0 672d6d61 6b652d72 656c6561 73655f30  g-make-release_0
 03e0 2f736e61 7073686f 74732f67 6c696263  /snapshots/glibc
 03f0 2e676974 7e72656c 65617365 7e322e32  .git~release~2.2
 0400 357e6d61 73746572 2f637375 00474e55  5~master/csu.GNU
 0410 20415320 322e3238 2e320001 80         AS 2.28.2...   
Contents of section .debug_abbrev:
 0000 01110010 06110112 0103081b 08250813  .............%..
 0010 05000000 01110125 0e130b03 0e1b0e10  .......%........
 0020 17000002 24000b0b 3e0b030e 00000324  ....$...>......$
 0030 000b0b3e 0b030800 00042600 49130000  ...>......&.I...
 0040 05340003 0e3a0b3b 0b49133f 19021800  .4...:.;.I.?....
 0050 00000111 00100655 0603081b 08250813  .......U.....%..
 0060 05000000 01110125 0e130b03 0e1b0e11  .......%........
 0070 01120710 17000002 24000b0b 3e0b030e  ........$...>...
 0080 00000316 00030e3a 0b3b0b49 13000004  .......:.;.I....
 0090 26004913 00000501 01491301 13000006  &.I......I......
 00a0 21000000 070f000b 0b491300 00081501  !........I......
 00b0 27190113 00000905 00491300 000a2400  '........I....$.
 00c0 0b0b3e0b 03080000 0b340003 0e3a0b3b  ..>......4...:.;
 00d0 0b49133f 193c1900 000c1500 27190000  .I.?.<......'...
 00e0 0d2e003f 19030e3a 0b3b0b27 19110112  ...?...:.;.'....
 00f0 07401897 42190000 0e2e013f 19030e3a  [email protected]......?...:
 0100 0b3b0b27 19110112 07401897 42190113  .;.'[email protected]...
 0110 00000f05 00030e3a 0b3b0b49 13021700  .......:.;.I....
 0120 00103400 030e3a0b 3b0b4913 02170000  ..4...:.;.I.....
 0130 110b0111 01120701 13000012 34000308  ............4...
 0140 3a0b3b0b 49130217 00001389 82010111  :.;.I...........
 0150 01000014 8a820100 02189142 18000015  ...........B....
 0160 89820100 11013113 0000162e 003f193c  ......1......?.<
 0170 196e0e03 0e3a0b3b 0b000000 01110010  .n...:.;........
 0180 06550603 081b0825 08130500 0000      .U.....%......  
Contents of section .debug_line:
 0000 56000000 02003100 00000401 fb0e0d00  V.....1.........
 0010 01010101 00000001 0000012e 2e2f7379  ............./sy
 0020 73646570 732f6161 72636836 34000073  sdeps/aarch64..s
 0030 74617274 2e530001 00000000 09026004  tart.S........`.
 0040 40000000 00000331 01212323 2123030d  @......1.!##!#..
 0050 20212127 23020800 01012300 00000200   !!'#.....#.....
 0060 1d000000 0401fb0e 0d000101 01010000  ................
 0070 00010000 0100696e 69742e63 00000000  ......init.c....
 0080 00780000 00020030 00000004 01fb0e0d  .x.....0........
 0090 00010101 01000000 01000001 2e2e2f73  ............../s
 00a0 79736465 70732f61 61726368 36340000  ysdeps/aarch64..
 00b0 63727469 2e530001 00000000 0902a804  crti.S..........
 00c0 40000000 0000033e 01212121 22020100  @......>.!!!"...
 00d0 01010009 02e80340 00000000 0003cc00  .......@........
 00e0 01212202 01000101 00090254 06400000  .!"........T.@..
 00f0 00000003 d9000121 02010001 01020100  .......!........
 0100 000200b8 00000004 01fb0e0d 00010101  ................
 0110 01000000 01000001 2f686f6d 652f7463  ......../home/tc
 0120 77672d62 75696c64 736c6176 652f776f  wg-buildslave/wo
 0130 726b7370 6163652f 74637767 2d6d616b  rkspace/tcwg-mak
 0140 652d7265 6c656173 655f302f 5f627569  e-release_0/_bui
 0150 6c642f62 75696c64 732f6465 73746469  ld/builds/destdi
 0160 722f7838 365f3634 2d756e6b 6e6f776e  r/x86_64-unknown
 0170 2d6c696e 75782d67 6e752f6c 69622f67  -linux-gnu/lib/g
 0180 63632f61 61726368 36342d6c 696e7578  cc/aarch64-linux
 0190 2d676e75 2f372e35 2e302f69 6e636c75  -gnu/7.5.0/inclu
 01a0 64650000 656c662d 696e6974 2e630000  de..elf-init.c..
 01b0 00007374 64646566 2e680001 00000000  ..stddef.h......
 01c0 0902d005 40000000 000003c3 00010312  ....@...........
 01d0 3c036e4a 03122003 6e200312 3c036e20  <.nJ.. .n ..<.n 
 01e0 030f2024 2e000204 03210002 04034900  .. $.....!....I.
 01f0 02040321 00020403 1f3e5f03 0a010201  ...!.....>_.....
 0200 0001015e 00000002 00300000 000401fb  ...^.....0......
 0210 0e0d0001 01010100 00000100 00012e2e  ................
 0220 2f737973 64657073 2f616172 63683634  /sysdeps/aarch64
 0230 00006372 746e2e53 00010000 00000902  ..crtn.S........
 0240 f4034000 00000000 03280121 02010001  ..@......(.!....
 0250 01000902 5c064000 00000000 032c0121  ....\.@......,.!
 0260 02010001 01                          .....           
Contents of section .debug_frame:
 0000 0c000000 ffffffff 01000478 1e0c1f00  ...........x....
 0010 3c000000 00000000 d0054000 00000000  <.........@.....
 0020 80000000 00000000 410e409d 089e0741  [email protected]
 0030 0d1d4194 05950447 96039702 98014793  ..A....G......G.
 0040 064ad344 deddd8d6 d7d4d50c 1f000000  .J.D............
 0050 14000000 00000000 50064000 00000000  ........P.@.....
 0060 04000000 00000000                    ........        
Contents of section .debug_str:
 0000 6c6f6e67 20756e73 69676e65 6420696e  long unsigned in
 0010 74007368 6f727420 756e7369 676e6564  t.short unsigned
 0020 20696e74 0073686f 72742069 6e74005f   int.short int._
 0030 494f5f73 7464696e 5f757365 6400756e  IO_stdin_used.un
 0040 7369676e 65642063 68617200 474e5520  signed char.GNU 
 0050 43313120 372e352e 30202d6d 61726368  C11 7.5.0 -march
 0060 3d61726d 76382d61 202d6d6c 6974746c  =armv8-a -mlittl
 0070 652d656e 6469616e 202d6d61 62693d6c  e-endian -mabi=l
 0080 70363420 2d67202d 4f32202d 7374643d  p64 -g -O2 -std=
 0090 676e7531 31202d66 676e7538 392d696e  gnu11 -fgnu89-in
 00a0 6c696e65 202d666d 65726765 2d616c6c  line -fmerge-all
 00b0 2d636f6e 7374616e 7473202d 66726f75  -constants -frou
 00c0 6e64696e 672d6d61 7468202d 666e6f2d  nding-math -fno-
 00d0 73746163 6b2d7072 6f746563 746f7220  stack-protector 
 00e0 2d66746c 732d6d6f 64656c3d 696e6974  -ftls-model=init
 00f0 69616c2d 65786563 002f686f 6d652f74  ial-exec./home/t
 0100 6377672d 6275696c 64736c61 76652f77  cwg-buildslave/w
 0110 6f726b73 70616365 2f746377 672d6d61  orkspace/tcwg-ma
 0120 6b652d72 656c6561 73655f30 2f736e61  ke-release_0/sna
 0130 7073686f 74732f67 6c696263 2e676974  pshots/glibc.git
 0140 7e72656c 65617365 7e322e32 357e6d61  ~release~2.25~ma
 0150 73746572 2f637375 006c6f6e 67206c6f  ster/csu.long lo
 0160 6e672069 6e74005f 5f696e69 745f6172  ng int.__init_ar
 0170 7261795f 73746172 74007369 7a655f74  ray_start.size_t
 0180 00474e55 20433131 20372e35 2e30202d  .GNU C11 7.5.0 -
 0190 6d617263 683d6172 6d76382d 61202d6d  march=armv8-a -m
 01a0 6c697474 6c652d65 6e646961 6e202d6d  little-endian -m
 01b0 6162693d 6c703634 202d6720 2d4f3220  abi=lp64 -g -O2 
 01c0 2d737464 3d676e75 3131202d 66676e75  -std=gnu11 -fgnu
 01d0 38392d69 6e6c696e 65202d66 6d657267  89-inline -fmerg
 01e0 652d616c 6c2d636f 6e737461 6e747320  e-all-constants 
 01f0 2d66726f 756e6469 6e672d6d 61746820  -frounding-math 
 0200 2d666e6f 2d737461 636b2d70 726f7465  -fno-stack-prote
 0210 63746f72 202d6650 4943202d 66746c73  ctor -fPIC -ftls
 0220 2d6d6f64 656c3d69 6e697469 616c2d65  -model=initial-e
 0230 78656300 656e7670 005f5f66 696e695f  xec.envp.__fini_
 0240 61727261 795f7374 61727400 656c662d  array_start.elf-
 0250 696e6974 2e63005f 5f6c6962 635f6373  init.c.__libc_cs
 0260 755f6669 6e69005f 5f66696e 695f6172  u_fini.__fini_ar
 0270 7261795f 656e6400 5f5f7072 65696e69  ray_end.__preini
 0280 745f6172 7261795f 73746172 74006172  t_array_start.ar
 0290 67630073 697a6500 5f5f6c69 62635f63  gc.size.__libc_c
 02a0 73755f69 6e697400 61726776 006c6f6e  su_init.argv.lon
 02b0 6720646f 75626c65 005f5f70 7265696e  g double.__prein
 02c0 69745f61 72726179 5f656e64 005f5f69  it_array_end.__i
 02d0 6e69745f 61727261 795f656e 6400      nit_array_end.  
Contents of section .debug_loc:
 0000 00000000 00000000 3b000000 00000000  ........;.......
 0010 0100503b 00000000 00000074 00000000  ..P;.......t....
 0020 00000001 00667400 00000000 00008000  .....ft.........
 0030 00000000 00000400 f301509f 00000000  ..........P.....
 0040 00000000 00000000 00000000 00000000  ................
 0050 00000000 3b000000 00000000 0100513b  ....;.........Q;
 0060 00000000 00000074 00000000 00000001  .......t........
 0070 00677400 00000000 00008000 00000000  .gt.............
 0080 00000400 f301519f 00000000 00000000  ......Q.........
 0090 00000000 00000000 00000000 00000000  ................
 00a0 3b000000 00000000 0100523b 00000000  ;.........R;....
 00b0 00000078 00000000 00000001 00687800  ...x.........hx.
 00c0 00000000 00008000 00000000 00000400  ................
 00d0 f301529f 00000000 00000000 00000000  ..R.............
 00e0 00000000 3c000000 00000000 70000000  ....<.......p...
 00f0 00000000 01006400 00000000 00000000  ......d.........
 0100 00000000 0000003c 00000000 00000048  .......<.......H
 0110 00000000 00000002 00309f48 00000000  .........0.H....
 0120 0000005c 00000000 00000001 00635c00  ...\.........c\.
 0130 00000000 00006000 00000000 00000300  ......`.........
 0140 837f9f60 00000000 0000006c 00000000  ...`.......l....
 0150 00000001 00630000 00000000 00000000  .....c..........
 0160 00000000 0000                        ......          
Contents of section .debug_ranges:
 0000 ffffffff ffffffff 00000000 00000000  ................
 0010 a8044000 00000000 bc044000 00000000  ..@.......@.....
 0020 e8034000 00000000 f4034000 00000000  ..@.......@.....
 0030 54064000 00000000 5c064000 00000000  T.@.....\.@.....
 0040 00000000 00000000 00000000 00000000  ................
 0050 ffffffff ffffffff 00000000 00000000  ................
 0060 f4034000 00000000 fc034000 00000000  ..@.......@.....
 0070 5c064000 00000000 64064000 00000000  \[email protected].@.....
 0080 00000000 00000000 00000000 00000000  ................

Disassembly of section .init:

00000000004003e8 <_init>:
  4003e8:	a9bf7bfd 	stp	x29, x30, [sp, #-16]!
  4003ec:	910003fd 	mov	x29, sp
  4003f0:	9400002e 	bl	4004a8 <call_weak_fn>
  4003f4:	a8c17bfd 	ldp	x29, x30, [sp], #16
  4003f8:	d65f03c0 	ret

Disassembly of section .plt:

0000000000400400 <.plt>:
  400400:	a9bf7bf0 	stp	x16, x30, [sp, #-16]!
  400404:	90000090 	adrp	x16, 410000 <__FRAME_END__+0xf970>
  400408:	f947fe11 	ldr	x17, [x16, #4088]
  40040c:	913fe210 	add	x16, x16, #0xff8
  400410:	d61f0220 	br	x17
  400414:	d503201f 	nop
  400418:	d503201f 	nop
  40041c:	d503201f 	nop

0000000000400420 <__libc_start_main@plt>:
  400420:	b0000090 	adrp	x16, 411000 <__libc_start_main@GLIBC_2.17>
  400424:	f9400211 	ldr	x17, [x16]
  400428:	91000210 	add	x16, x16, #0x0
  40042c:	d61f0220 	br	x17

0000000000400430 <__gmon_start__@plt>:
  400430:	b0000090 	adrp	x16, 411000 <__libc_start_main@GLIBC_2.17>
  400434:	f9400611 	ldr	x17, [x16, #8]
  400438:	91002210 	add	x16, x16, #0x8
  40043c:	d61f0220 	br	x17

0000000000400440 <abort@plt>:
  400440:	b0000090 	adrp	x16, 411000 <__libc_start_main@GLIBC_2.17>
  400444:	f9400a11 	ldr	x17, [x16, #16]
  400448:	91004210 	add	x16, x16, #0x10
  40044c:	d61f0220 	br	x17

0000000000400450 <printf@plt>:
  400450:	b0000090 	adrp	x16, 411000 <__libc_start_main@GLIBC_2.17>
  400454:	f9400e11 	ldr	x17, [x16, #24]
  400458:	91006210 	add	x16, x16, #0x18
  40045c:	d61f0220 	br	x17

Disassembly of section .text:

0000000000400460 <_start>:
  400460:	d280001d 	mov	x29, #0x0                   	// #0
  400464:	d280001e 	mov	x30, #0x0                   	// #0
  400468:	aa0003e5 	mov	x5, x0
  40046c:	f94003e1 	ldr	x1, [sp]
  400470:	910023e2 	add	x2, sp, #0x8
  400474:	910003e6 	mov	x6, sp
  400478:	580000c0 	ldr	x0, 400490 <_start+0x30>
  40047c:	580000e3 	ldr	x3, 400498 <_start+0x38>
  400480:	58000104 	ldr	x4, 4004a0 <_start+0x40>
  400484:	97ffffe7 	bl	400420 <__libc_start_main@plt>
  400488:	97ffffee 	bl	400440 <abort@plt>
  40048c:	00000000 	.inst	0x00000000 ; undefined
  400490:	00400584 	.word	0x00400584
  400494:	00000000 	.word	0x00000000
  400498:	004005d0 	.word	0x004005d0
  40049c:	00000000 	.word	0x00000000
  4004a0:	00400650 	.word	0x00400650
  4004a4:	00000000 	.word	0x00000000

00000000004004a8 <call_weak_fn>:
  4004a8:	90000080 	adrp	x0, 410000 <__FRAME_END__+0xf970>
  4004ac:	f947f000 	ldr	x0, [x0, #4064]
  4004b0:	b4000040 	cbz	x0, 4004b8 <call_weak_fn+0x10>
  4004b4:	17ffffdf 	b	400430 <__gmon_start__@plt>
  4004b8:	d65f03c0 	ret
  4004bc:	00000000 	.inst	0x00000000 ; undefined

00000000004004c0 <deregister_tm_clones>:
  4004c0:	b0000080 	adrp	x0, 411000 <__libc_start_main@GLIBC_2.17>
  4004c4:	9100e000 	add	x0, x0, #0x38
  4004c8:	b0000081 	adrp	x1, 411000 <__libc_start_main@GLIBC_2.17>
  4004cc:	9100e021 	add	x1, x1, #0x38
  4004d0:	eb00003f 	cmp	x1, x0
  4004d4:	540000a0 	b.eq	4004e8 <deregister_tm_clones+0x28>  // b.none
  4004d8:	90000001 	adrp	x1, 400000 <_init-0x3e8>
  4004dc:	f9433821 	ldr	x1, [x1, #1648]
  4004e0:	b4000041 	cbz	x1, 4004e8 <deregister_tm_clones+0x28>
  4004e4:	d61f0020 	br	x1
  4004e8:	d65f03c0 	ret
  4004ec:	d503201f 	nop

00000000004004f0 <register_tm_clones>:
  4004f0:	b0000080 	adrp	x0, 411000 <__libc_start_main@GLIBC_2.17>
  4004f4:	9100e000 	add	x0, x0, #0x38
  4004f8:	b0000081 	adrp	x1, 411000 <__libc_start_main@GLIBC_2.17>
  4004fc:	9100e021 	add	x1, x1, #0x38
  400500:	cb000021 	sub	x1, x1, x0
  400504:	9343fc21 	asr	x1, x1, #3
  400508:	8b41fc21 	add	x1, x1, x1, lsr #63
  40050c:	9341fc21 	asr	x1, x1, #1
  400510:	b40000a1 	cbz	x1, 400524 <register_tm_clones+0x34>
  400514:	90000002 	adrp	x2, 400000 <_init-0x3e8>
  400518:	f9433c42 	ldr	x2, [x2, #1656]
  40051c:	b4000042 	cbz	x2, 400524 <register_tm_clones+0x34>
  400520:	d61f0040 	br	x2
  400524:	d65f03c0 	ret

0000000000400528 <__do_global_dtors_aux>:
  400528:	a9be7bfd 	stp	x29, x30, [sp, #-32]!
  40052c:	910003fd 	mov	x29, sp
  400530:	f9000bf3 	str	x19, [sp, #16]
  400534:	b0000093 	adrp	x19, 411000 <__libc_start_main@GLIBC_2.17>
  400538:	3940e260 	ldrb	w0, [x19, #56]
  40053c:	35000080 	cbnz	w0, 40054c <__do_global_dtors_aux+0x24>
  400540:	97ffffe0 	bl	4004c0 <deregister_tm_clones>
  400544:	52800020 	mov	w0, #0x1                   	// #1
  400548:	3900e260 	strb	w0, [x19, #56]
  40054c:	f9400bf3 	ldr	x19, [sp, #16]
  400550:	a8c27bfd 	ldp	x29, x30, [sp], #32
  400554:	d65f03c0 	ret

0000000000400558 <frame_dummy>:
  400558:	17ffffe6 	b	4004f0 <register_tm_clones>

000000000040055c <func>:
  40055c:	a9be7bfd 	stp	x29, x30, [sp, #-32]!
  400560:	910003fd 	mov	x29, sp
  400564:	b9001fa0 	str	w0, [x29, #28]
  400568:	90000000 	adrp	x0, 400000 <_init-0x3e8>
  40056c:	911a0000 	add	x0, x0, #0x680
  400570:	b9401fa1 	ldr	w1, [x29, #28]
  400574:	97ffffb7 	bl	400450 <printf@plt>
  400578:	d503201f 	nop
  40057c:	a8c27bfd 	ldp	x29, x30, [sp], #32
  400580:	d65f03c0 	ret

0000000000400584 <main>:
  400584:	a9be7bfd 	stp	x29, x30, [sp, #-32]!
  400588:	910003fd 	mov	x29, sp
  40058c:	528000c0 	mov	w0, #0x6                   	// #6
  400590:	b9001fa0 	str	w0, [x29, #28]
  400594:	b0000080 	adrp	x0, 411000 <__libc_start_main@GLIBC_2.17>
  400598:	9100d000 	add	x0, x0, #0x34
  40059c:	b9400001 	ldr	w1, [x0]
  4005a0:	b0000080 	adrp	x0, 411000 <__libc_start_main@GLIBC_2.17>
  4005a4:	9100f000 	add	x0, x0, #0x3c
  4005a8:	b9400000 	ldr	w0, [x0]
  4005ac:	0b000021 	add	w1, w1, w0
  4005b0:	b9401fa0 	ldr	w0, [x29, #28]
  4005b4:	0b000021 	add	w1, w1, w0
  4005b8:	b9401ba0 	ldr	w0, [x29, #24]
  4005bc:	0b000020 	add	w0, w1, w0
  4005c0:	97ffffe7 	bl	40055c <func>
  4005c4:	b9401fa0 	ldr	w0, [x29, #28]
  4005c8:	a8c27bfd 	ldp	x29, x30, [sp], #32
  4005cc:	d65f03c0 	ret

00000000004005d0 <__libc_csu_init>:
  4005d0:	a9bc7bfd 	stp	x29, x30, [sp, #-64]!
  4005d4:	910003fd 	mov	x29, sp
  4005d8:	a901d7f4 	stp	x20, x21, [sp, #24]
  4005dc:	90000094 	adrp	x20, 410000 <__FRAME_END__+0xf970>
  4005e0:	90000095 	adrp	x21, 410000 <__FRAME_END__+0xf970>
  4005e4:	91380294 	add	x20, x20, #0xe00
  4005e8:	9137e2b5 	add	x21, x21, #0xdf8
  4005ec:	a902dff6 	stp	x22, x23, [sp, #40]
  4005f0:	cb150294 	sub	x20, x20, x21
  4005f4:	f9001ff8 	str	x24, [sp, #56]
  4005f8:	2a0003f6 	mov	w22, w0
  4005fc:	aa0103f7 	mov	x23, x1
  400600:	9343fe94 	asr	x20, x20, #3
  400604:	aa0203f8 	mov	x24, x2
  400608:	97ffff78 	bl	4003e8 <_init>
  40060c:	b4000194 	cbz	x20, 40063c <__libc_csu_init+0x6c>
  400610:	f9000bb3 	str	x19, [x29, #16]
  400614:	d2800013 	mov	x19, #0x0                   	// #0
  400618:	f8737aa3 	ldr	x3, [x21, x19, lsl #3]
  40061c:	aa1803e2 	mov	x2, x24
  400620:	aa1703e1 	mov	x1, x23
  400624:	2a1603e0 	mov	w0, w22
  400628:	91000673 	add	x19, x19, #0x1
  40062c:	d63f0060 	blr	x3
  400630:	eb13029f 	cmp	x20, x19
  400634:	54ffff21 	b.ne	400618 <__libc_csu_init+0x48>  // b.any
  400638:	f9400bb3 	ldr	x19, [x29, #16]
  40063c:	a941d7f4 	ldp	x20, x21, [sp, #24]
  400640:	a942dff6 	ldp	x22, x23, [sp, #40]
  400644:	f9401ff8 	ldr	x24, [sp, #56]
  400648:	a8c47bfd 	ldp	x29, x30, [sp], #64
  40064c:	d65f03c0 	ret

0000000000400650 <__libc_csu_fini>:
  400650:	d65f03c0 	ret

Disassembly of section .fini:

0000000000400654 <_fini>:
  400654:	a9bf7bfd 	stp	x29, x30, [sp, #-16]!
  400658:	910003fd 	mov	x29, sp
  40065c:	a8c17bfd 	ldp	x29, x30, [sp], #16
  400660:	d65f03c0 	ret

参考文献:

[1]. Meaning of "CONTENTS, ALLOC, LOAD, READONLY, CODE" in ELF sections - Lynxbee

[2]. aarch64-linux-gnu-objdump(1) — Arch manual pages (archlinux.org)

[3]. aarch64-linux-gnu-objcopy(1) — Arch manual pages (archlinux.org)

[4]. ELF for the Arm 64-bit Architecture (AArch64) - ABI 2020Q2 documentation

基于OMAPL138的Linux字符驱动_GPIO驱动AD9833(一)之miscdevice和ioctl

基于OMAPL138的Linux字符驱动_GPIO驱动AD9833(一)之miscdevice和ioctl

0. 导语

在嵌入式的道路上寻寻觅觅很久,进入嵌入式这个行业也有几年的时间了,从2011年后半年开始,我清楚的记得当时拿着C51的板子闪烁了LED灯,从那时候开始,就进入到了嵌入式的大门里面。嵌入式的学习从来没有停止过,中间也有无数的插曲和机缘巧合学会C++和Java,做一些好玩的应用。无论是嵌入式DSP也好,还是如今的嵌入式ARM,7年之久从来没有停止过。技术最大的好处就是,**无论发展到什么境地,那种第一次点亮LED灯欣喜永远的可以伴随着你,只要你解决了一个卡了你很久的问题,这就是技术的魅力。**我也将开始大肆的从嵌入式DSP转入到嵌入式Linux,在研究生阶段,完成这个转型。

这个Demo意义重大,使用Linux也有四五年的时间了,Linux良好的基础和嵌入式基础让我在嵌入式inux道路上算的上是顺风顺水。**这个Demo将过去STM32,F28xx的DSP或者那些单片机桥接起来,将过去裸机上的程序全部编到内核里面,通过嵌入式的应用进行互联。 **

本DEMO依然使用AD9833作为例子,将用linux内核级的gpio对AD9833写时序,完成对于AD9833的驱动程序,在嵌入式Linux上生成/dev/目录节点,使用Linux命令行对AD9833产生波形进行控制。(只要有了/dev节点,使用Qt,C++,Python都可以控制了,这就是物联网最注重的。)

效果视频观看地址: https://v.youku.com/v_show/id_XMzY3NDUwNTMwOA==.html?spm=a2h3j.8428770.3416059.1

1. 开发驱动综述

本开发驱动基于Linux3.3内核版本,且内核必须编译正确,否则不能运行。
这个Demo可以归结为三个部分,一个部分为Linux字符驱动模板,第二部分为AD9833驱动程序,第三部分为通信协议。还附加一个配置文件。

  • Linux字符驱动模板主要包含init exit 还有ioctl,函数;
  • AD9833驱动程序为AD9833的GPIO时序(AD9833为SPI协议,这里先用GPIO模拟时序,后续升级为SPI外设);
  • 通信协议格式方式,用户对于AD9833的控制字,比如发送波形命令,频率命令等;
  • 将自己编写的驱动写入内核的代码树,编译成模块或者编译进内核随内核启动;

本Demo就围绕这三点进行。

2. Linux字符驱动模板

* 函数ioctl

主要负责进行数据交互的。当设备生成字符设备驱动节点(/dev目录下),使用shell级命令cat或者编译一段C应用程序用open打开节点的时候,后面将参数就是通过ioctl函数进行传递。(在嵌入式Linux端定义一个ioctl的函数,在C语言的程序也有一个ioctl用来和其进行对应,这样就完成了数据参数传递。)

*结构体file_operations

static int
ad9833_ioctl(struct file  *file, unsigned int cmd, unsigned long arg )
{

	printk(DRV_NAME "\tRecv cmd: %u\n", cmd);
	printk(DRV_NAME "\tRecv arg: %lu\n", arg);
	switch( cmd ) {
	case CMD_TYPE_SIN:
		ad9833->set_wave_freq(ad9833, 1500);
		ad9833->set_wave_type(ad9833, SIN);
		printk( DRV_NAME " set wave is sine wave! arg = %lu\n" , arg );

		break;

	case CMD_TYPE_TRI:
		ad9833->set_wave_freq(ad9833, 1500);
		ad9833->set_wave_type(ad9833, TRI);
		printk( DRV_NAME " set wave is tri wave! arg = %lu\n" , arg );
		break;

	case CMD_TYPE_SQE:
		ad9833->set_wave_freq(ad9833, 1500);
		ad9833->set_wave_type(ad9833, SQU);
		printk( DRV_NAME " set wave is sw wave! arg = %lu\n" , arg );
		break;

	}
	return	0;
}

ioctl函数不能独立的存在需要file_operations指针进行操作,ioctl为一个执行命令的清单,file_operations就是这个清单的执行者。下面就是file_operations的指针,里面的成员需要接收到ad9833_ioctl的函数地址,在内部运行的时候会调用该地址。

static struct file_operations ad9833_fops = {

		.owner				=	THIS_MODULE,
		.unlocked_ioctl  	=  	ad9833_ioctl,
};

###* 结构体miscdevice
*miscdevice结构体为字符驱动的一级,字符驱动如同文献[3]所说的一样,非常的凌乱,到底里面使用了miscdevice还是cdev还是platform-device or platform-driver,这里暂时不进行理,这里使用miscdevice级的字符驱动设备向Linux内核进行设备的注册,后续有文章进行区分,类似的文献还有我的《Linux GPIO键盘驱动开发记录_OMAPL138》,这里使用的室platform-device进行。

static struct miscdevice ad9833_miscdev  = {
		// DRV_NAME 在前面进行define
		// #define	DRV_NAME 	"AD9833-ADI"
		.name				=	DRV_NAME,
		.fops				=	&ad9833_fops,
};

可以看见,在miscdev里面指定了file指针的地址,miscdev主要的作用就是向内核注册该驱动

*函数init

内核级的嵌入式Linux驱动给出的硬性要求进行init函数,并标识init函数为__init,而且还要在module_init中填写init函数的地址。

static int __init ad9833_dev_init( void )
{
	int  i,ret;

	/*
	 * AD9833 new device
	 * */
	printk( DRV_NAME "\tApply memory for AD9833.\n" );
	ad9833 = ad9833_dev_new();

	/*
	 * AD9833 init gpios.
	 * */
	printk( DRV_NAME "\tInititial GPIO\n" );

	for ( i = 0; i < 3; i ++ ) {
		ret	=	gpio_request( ad9833_gpios[i], "AD9833 GPIO" );
		if( ret ) {
			printk("\t%s: request gpio %d for AD9833 failed, ret = %d\n", DRV_NAME,ad9833_gpios[i],ret);
			return ret;
		}else {
			printk("\t%s: request gpio %d for AD9833 set succussful, ret = %d\n", DRV_NAME,ad9833_gpios[i],ret);
		}
		gpio_direction_output( ad9833_gpios[i],1 );
		gpio_set_value( ad9833_gpios[i],0 );
	}

	ret = misc_register( &ad9833_miscdev );
	printk( DRV_NAME "\tinitialized\n" );
	return ret;
}

module_init( ad9833_dev_init );

当我们运行insmod xxxx.ko的时候,此时运行的就是这个init函数,在这个函数中主要完成对于设备内存的请求和一些初始状态的注册。在本DEMO中对对于AD9833的结构体进行了注册,并对gpio进行申请。ret = misc_register( &ad9833_miscdev ); 重点室这句话。

*函数exit

除此之外内核也要求了exit函数,主要进行对init中内存申请的释放。

static void __exit ad9833_dev_exit( void )
{
	int i;
	for( i = 0; i < 3; i++) {
		gpio_free( ad9833_gpios[i] );
	}
	misc_deregister( &ad9833_miscdev );

}
module_exit( ad9833_dev_exit );

这是一个非常简单的字符驱动的模板,然后就需要我们添加AD9833的驱动了。

3. AD9833芯片级时序驱动

到此,基本上就是裸机嵌入式的知识了,对于芯片功能的描述,对于芯片时序的把握。作为本博客不在赘述,给出函数的列表,如果喜欢,本文将DEMO的源码放在后面,自行下载观看。

static void ad9833_set_wave_type( AD9833 *dev, enum ad9833_wavetype_t wave_type );
static void ad9833_set_phase( AD9833 *dev, unsigned int phase_value );
static void ad9833_set_freq( AD9833 *dev, float freq );
static void ad9833_set_para( AD9833 *dev, unsigned long freqs_value, unsigned int phase_value, enum ad9833_wavetype_t wave_type );
static void ad9833_init_device( AD9833 *dev ) ;
static void ad9833_write_reg( AD9833 *dev, unsigned int reg_value );
static int 	ad9833_ioctl(struct file  *file, unsigned int cmd, unsigned long arg );
AD9833 *ad9833;

AD9833 *ad9833_dev_new()
{
	AD9833 *dev = (AD9833*)kcalloc(1, sizeof(AD9833), GFP_ATOMIC);

	dev->hw.fsy			  =	  AD9833_FSY_IO;
	dev->hw.sdi			  =   AD9833_DAT_IO;
	dev->hw.clk			  =	  AD9833_CLK_IO;

	dev->set_wave_para    =   &ad9833_set_para;
	dev->init_device      =   &ad9833_init_device;
	dev->write_reg        =   &ad9833_write_reg;
	dev->set_wave_freq    =   &ad9833_set_freq;
	dev->set_wave_phase	  =   &ad9833_set_phase;
	dev->set_wave_type    =   &ad9833_set_wave_type;
	dev->init_device( dev );


	return dev;
}

该设备使用链表进行描述。

3. 与驱动通信的ioctl函数

在参考文献[1]中,给出了Linux字符设备驱动开发重要的ioctl函数解析,写的很接地气,很朴实,也写的很明白,包括利用ioctl函数应用程序和驱动程序进行交互,ioctl函数使用MAGIC_number幻数对命令进行转换。
在ioctrl函数里面通常使用switch 和case进行执行,见上衣章的内容。
这里给出使用ioctl的应用程序,它和内核驱动进行通信:


#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

#define				AD9833_MAGIC				'k'
#define				CMD_TYPE_SIN				_IO( AD9833_MAGIC, 0)
#define				CMD_TYPE_TRI				_IO( AD9833_MAGIC, 1)
#define				CMD_TYPE_SQE				_IO( AD9833_MAGIC, 2)


const char dev_path[]="/dev/AD9833-ADI";

int main(int argc , char *argv[])
{

    int fd = -1, i = 0;
    printf("ad9833 test program run....\n");


    fd = open(dev_path, O_RDWR|O_NDELAY);  // 打开设备
    if (fd < 0) {
        printf("Can't open /dev/AD9833-ADI\n");
        return -1;
    }

    printf("open device.\n");

    if( strcmp(argv[1],"1") == 0 ) {
	ioctl(fd, CMD_TYPE_SIN, 5);
		printf("argc = %d,sine wave = %s \n", CMD_TYPE_SIN, argv[1]);
    }else if(  strcmp(argv[1],"2") == 0 ) {
		ioctl(fd, CMD_TYPE_TRI, 1);
		printf("argc = %d,tri wave = %s \n", CMD_TYPE_TRI,argv[1]);
    }else{
 		ioctl(fd, CMD_TYPE_SQE, 1);
		printf("argc = %d,sqe wave = %s \n", CMD_TYPE_SQE, argv[1]);
    }
    
    printf("argc = %d\n", argc);
    close(fd);
    return 0;
}

在ioctl函数和嵌入式Linux驱动里面的ioctl函数就会对应,命令就传递过去了。

另外补充一个知识:

void main( int argc char *argv[] )

  • argc 为传递参数的个数
  • argv[1] 为一个字符串,第一个传递进来的字符串,比如 ./main.o nihao hello 1234
    argv[1] 就是nihao, argv[2] 就是hello, argv[3] 就是1234

4. 将驱动程序编入Linux内核代码树

驱动开发完毕,就必须要将驱动编入Linux内核代码树,假如Linux内核代码在./linux-3.3目录,我们的驱动名字叫做ad9833.c,那么我们就要将ad9833.c文件放入./linux-3.3/drivers/char目录下,操作两件事情。

修改Kconfig文件

修改Kconfig文件,在menuconfig文件中会出现我们的内核配置选项。

config  AD9833_ADI
        tristate "AD9833 DDS support."
        depends on ARM
        help
          GPIO on OMAPL138 configuration is:
          AD9833_FSY_IO -> GPIO[0,1]
          AD9833_CLK_IO -> GPIO[0,5]
          AD9833_DAT_IO -> GPIO[0,0]
  • tristate: 内核在linux menuconfig菜单下显示的名字
  • depends on ARM: 只有在ARM架构下才会显示出来该驱动于menuconfig中
  • help :帮助文档,做一些提示,我这里给出了GPIO的接法。

修改该目录下的Makefile文件

在文末追加
obj-$(CONFIG_AD9833_ADI) += ad9833.o
这里CONFIG_后面接的必须和上面的Kconfig中 config字段一样 ad9833.o 的.o文件必须和放入该内核代码的ad9833.c名字字段一样。

编译内核

  • 配置menuconfig
    make CROSS_COMPILE=arm-none-linux-gnueabi- ARCH=arm menuconfig
    然后,进入到drivers -> char.. device -> 找到你的驱动
    以模块编译或者编译进内核。
  • 编译内核
    make CROSS_COMPILE=arm-none-linux-gnueabi- ARCH=arm -j8
  • 生成uImage文件 (这个是omapl平台要求的)
    make CROSS_COMPILE=arm-none-linux-gnueabi- ARCH=arm uImage
  • 将内核和文件都放到目标板子
    可以重启运行了
  • 加载内核
    insmod ad9833.ko
  • 运行测试程序
    可以看到效果了:

源代码下载

链接: https://pan.baidu.com/s/1rfZymtf-mRnZNlhb41RpGA 密码: 4pxx

参考文献

[1] zqixiao_09, Linux 字符设备驱动开发基础(四)—— ioctl() 函数解析
, 2016-03-11
[2] 草根老师, 解决undefined reference to __aeabi_uidivmod和undefined reference to __aeabi_uidiv'错误, 2012-07-21 21:59:03
[3] 小C爱学习, 一步一步写miscdevice的驱动模块, 2013-07-24
[4] 宋宝华,Linux设备驱动开发详解:基于最新的Linux 4.0内核

基于OMAPL:Linux3.3内核的编译

基于OMAPL:Linux3.3内核的编译

OMAPL对应3个版本的linux源代码,分别是:Linux-3.3、Linux-2.6.37、Linux2.6.33,这里的差距在于Linux2,缺少SYSLINK支持组件。

这里我们选择Linux-3.3版本进行开发。

开发前准备

  • mkimage的工具:sudo apt-get install u-boot-tools
  • menuconfig组件库安装:apt-get install libncurses5-dev
  • 正确配置arm-none-linux-gnueabi的环境
  • 内核源文件:linux-3.3.tar.bz2(一定要用TI提供配套OMAPL的,不是随便找个Linux3.3就可以的)

内核编译过程

  • 解压内核到~/work内核路径为~/work/linux-3.3

  • 进入Linux内核路径`~/work/linux-3.3

  • 清理内核(一个字儿都不能少)

    make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabi- distclean

配置内核

配置内核很关键,有些童鞋说,人家的内核编译了5分钟就结束了,怎么我的1个小时还没编译完,很有可能就是很多地方对于这个板子没有用的部件你没有删除掉,一般原厂都会有个推荐配置,当然了,TI的OMAPL138也是提供了推荐配置的。

创龙公司给出的配置为,执行命令:

make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabi- da850_omapl138_tl_defconfig

我使用的是德州仪器给的配置,则执行命令:

make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabi- da850_omapl138_defconfig

当然这不是瞎编的,内核中包含的配置文件在,以下的目录:

./arch/arm/configs

ls以下会看到很多配置:

我们使用画横线的配置信息。

  • 使用make menuconfig写入配置信息

make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabi- menuconfig

直接Exit就可以,然后就会写入配置信息了。

在这个menu里面我们配置驱动或者内核其他的组件机制,是否编译到内核中,这个根据自己需求进行,初学者可能对于这个地方没有多少概念。可参考韦东山《嵌入式Linux完全开发手册》的编译内核章节,上面有目录规则和解说等。

编译内核

  1. 先编译析出zImage文件
    make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabi- -j4
    2)将zImage转为uImage
    make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabi- uImage -j4

正在编译的内核

编译内核一般都会有各种各样的错误,去百度或者google清查。

编译成功生成uImage文件

编译完成后可以在内核源码的"arch/arm/boot"目录下找到 uImage。可以将 uImage 拷贝到正常使用的 SD 系统启动卡的 FAT32 格式的 boot 分区,方便以后 SD 卡启动方式时被 U-Boot 加载或者被烧写到 NAND FLASH 分区。

uImage文件

参考文献

Linux进程之间的通信-内存共享(System V)

Linux进程之间的通信-内存共享(System V)

  • 信号量 (sem) : 管理资源的访问
  • 共享内存 (shm): 高效的数据分享
  • 消息队列 (msg):在进程之间简易的传数据的方法
  • 互斥算法(Dekker, Peterson, Filter, Szymanski, Lamport面包店算法)

IPC(Inter-Process Communication,进程间通讯)包含三种通信方式,信号量、共享内存和消息队列。在linux编程里面可以有两个不同的标准,一个是SYSTEM-V标准,一个是POSIX标准。以下是两个标准之间的区别1。简单的说,POSIX更轻量,常面向于线程;SYSTEM-V更重一些,需要深陷Linux内核之中,面向于进程。

1. 内存共享

在大数据量的交换领域,使用memcpy是一个十分占据CPU和内存带宽的方法,共享内存作为Linux进程之间的通信是一个比较高效和节约资源的方法。现代多核心处理器,还有一个处理方法,比如对于视频的处理VPU,设定专门VDMA的RTL来处理。

1.1 原理

内存共享是随内核持续的,在两个毫无关系的进程中在userspace区域创建一个一块共享的内存,使两个进程都有读写的权限。既然是共享,必然涉及资源的竞争问题,故还要引入随内核持续的进程同步机制,来辅助访问共享的内存空间。如图所示,为进程的LMA在共享同一块share memory。

image-20220401121808422

既然是内存,这里就不得不提到,shm机制到底从哪里分配的内存,是heap还是stack。这里借用一个这个人的问题:1

Understand-shmat-and-attachment-to-the-process-memory?

It is recommended/conventional that the second argument to shmat(int id , void * addr,int flg) should be NULL.

But if i want to give it a specific address (void* addr), should that address be from the stack or the heap?

I mean do i have to malloc() and then pass that address to shmat or i can just declare void * adrr(or char * addr) and pass it to shmat.

9ZHwD

If addr is NULL, the system selects the first available address without corrupting the BSS segment. Most probably that will be in the heap. So you don't need to allocate.

If addr is from the stack segment of your application, calling shmat will corrupt the stack. Most probably that will result in a segmentation fault in your program. shmat will overwrite the variables on the stack, located at addresses lower than the one you gave as parameter.

  • 如果addr是NULL,系统紧凑选择不会覆盖BSS段展开的地址空间,最可能的是heap。
  • 如果addr是一个栈地址,shmat将会覆盖栈空间,可能会造成段错误。

因此,shmat会绕开BSS展开的空间,紧凑地在HEAP上分配空间

1.2 APIs

1.2.1 shmat, shmdt2

shmat() attaches the System V shared memory segment identified by shmid to the address space of the calling process.

#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
int shmdt(const void *shmaddr);

Parameters:

Params I/O Details
int shmid Input 由shmget返回的id值
const void *shmaddr Input 指定共享内存连接到当前进程中的地址的位置。下面引用手册中的,可以看到shmaddr是NULL或者是其他情况。
int shmflg Input SHM_RND[与addr联合使用,用于控制共享内存的连接地址] 或者 SHM_RDONLY[连接的内存只读]
  • If shmaddr is NULL, the system chooses a suitable (unused) page-aligned address to attach the segment.
  • If shmaddr isn't NULL and SHM_RND is specified in shmflg, the attach occurs at the address equal to shmaddr rounded down to the nearest multiple of SHMLBA.
  • Otherwise, shmaddr must be a page-aligned address at which the attach occurs.

Return:

On success, shmat() returns the address of the attached shared memory segment; on error, (void *) -1 is returned, and errno is set to indicate the error.

On success, shmdt() returns 0; on error -1 is returned, and errno is set to indicate the error.

Note:

  • After a fork(2), the child inherits the attached shared memory segments.
  • After an execve(2), all attached shared memory segments are detached from the process.
  • Upon _exit(2), all attached shared memory segments are detached from the process.

1.2.2 shmget3

shmget() returns the identifier of the System V shared memory segment associated with the value of the argument key. It may be used either to obtain the identifier of a previously created shared memory segment (when shmflg is zero and key does not have the value IPC_PRIVATE), or to create a new set.

A new shared memory segment, with size equal to the value of size rounded up to a multiple of PAGE_SIZE, is created if key has the value IPC_PRIVATE or key isn't IPC_PRIVATE, no shared memory segment corresponding to key exists, and IPC_CREAT is specified in shmflg.

If shmflg specifies both IPC_CREAT and IPC_EXCL and a shared memory segment already exists for key, then shmget() fails with errno set to EEXIST. (This is analogous to the effect of the combination O_CREAT | O_EXCL for open(2).)

#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);

Parameters:

Params I/O Details
key_t key Input 提供一个key
size_t size Input 需要共享内存的容量
int shmflg Input **IPC_CREAT **/IPC_EXCL /**SHM_HUGETLB **/SHM_HUGE_2MB /SHM_HUGE_1GB/ SHM_NORESERVE
  • If shmaddr is NULL, the system chooses a suitable (unused) page-aligned address to attach the segment.
  • If shmaddr isn't NULL and SHM_RND is specified in shmflg, the attach occurs at the address equal to shmaddr rounded down to the nearest multiple of SHMLBA.
  • Otherwise, shmaddr must be a page-aligned address at which the attach occurs.

Return:

On success, a valid shared memory identifier is returned. On error, -1 is returned, and errno is set to indicate the error.

Note:

The following limits on shared memory segment resources affect the shmget() call:

  • SHMALL,系统宽度限制总共的资源系统页大小统计。
  • SHMMAX,共享最大段空间
  • SHMMIN,共享最小段空间大小,1byte

具体参考文献3

1.2.3 shmctl4

shmctl() performs the control operation specified by cmd on the System V shared memory segment whose identifier is given in shmid.

#include <sys/shm.h>

struct shmid_ds {
    struct ipc_perm shm_perm;    /* Ownership and permissions */
    size_t          shm_segsz;   /* Size of segment (bytes) */
    time_t          shm_atime;   /* Last attach time */
    time_t          shm_dtime;   /* Last detach time */
    time_t          shm_ctime;   /* Creation time/time of last
                                    modification via shmctl() */
    pid_t           shm_cpid;    /* PID of creator */
    pid_t           shm_lpid;    /* PID of last shmat(2)/shmdt(2) */
    shmatt_t        shm_nattch;  /* No. of current attaches */
    ...
};
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

Parameters:

Params I/O Details
int shmid Input get返回的shmid
int cmd Input 采取的命令行动。 IPC_STAT,把shmid_ds结构中的数据设置为共享内存的当前关联值; IPC_SET,如果进程有足够的权限,就把共享内存的当前关联值设定为shmid_ds结构中给出的值;IPC_RMID,删除共享内存段。
struct shmid_ds *buf Input 指向包含共享内存模式和访问权限的结构,具体参考4
  • If shmaddr is NULL, the system chooses a suitable (unused) page-aligned address to attach the segment.
  • If shmaddr isn't NULL and SHM_RND is specified in shmflg, the attach occurs at the address equal to shmaddr rounded down to the nearest multiple of SHMLBA.
  • Otherwise, shmaddr must be a page-aligned address at which the attach occurs.

Return:

0 成功, -1失败。

1.3 Example

test_shm_1.c: server,消耗数据client的数据

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <sys/shm.h>
#include "test_shm.h"

#define debug_log printf("%s:%d--", __FUNCTION__, __LINE__);printf

int main(int argc, char *argv[])
{
    int i, ret;
    char op_chars[20];
    int count = 0;
    int shm_id = 0;
    int running = 1;

    void *share_memory = NULL;
    struct shared_use_st *share_stuff = NULL;

    debug_log("call the shmget function\n");
    shm_id = shmget((key_t)1234, sizeof(struct shared_use_st), 0666|IPC_CREAT);
    if (shm_id < 0) {
        debug_log("failed on semget\n");
        goto finish2;
    }

    debug_log("call the shmat\n");
    share_memory = shmat(shm_id, NULL, 0);
    if (share_memory == NULL - 1) {
        debug_log("failed on shmat\n");
        goto finish2;
    }
    debug_log("allocate memory attached at %x and main entry %x\n", (int)share_memory, (int)&main);
    share_stuff = (struct shared_use_st *)share_memory;
    share_stuff->written_by_you = 0;
    while(running) {
        if (share_stuff->written_by_you) {
            debug_log("You wrote: %s", share_stuff->some_text);
            sleep( rand() % 4);
            share_stuff->written_by_you = 0;
            if (strncmp(share_stuff->some_text, "end", 3) == 0) {
                running = 0;
            }
        }
    }
    debug_log("finish.....\n");
finish2:
    if (shmdt(share_memory) == -1) {
        debug_log("failed on share_memory\n");
        goto finish1;
    }
finish1:
    if (shmctl(shm_id, IPC_RMID, 0) == -1) {
        debug_log("failed on shmctl\n");
    }
    debug_log("finish test...\n");
    return ret;
}

test_shm_2.c: client,从stdin读入数据传送给server

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <sys/shm.h>
#include "test_shm.h"

#define debug_log printf("%s:%d--", __FUNCTION__, __LINE__);printf

int main(int argc, char *argv[])
{
    int i, ret;
    char op_chars[20];
    int count = 0;
    int shm_id = 0;
    int running = 1;
    char buffer[BUFSIZ];

    void *share_memory = NULL;
    struct shared_use_st *share_stuff = NULL;

    debug_log("call the shmget function\n");
    shm_id = shmget((key_t)1234, sizeof(struct shared_use_st), 0666|IPC_CREAT);
    if (shm_id < 0) {
        debug_log("failed on semget\n");
        goto finish2;
    }

    debug_log("call the shmat\n");
    share_memory = shmat(shm_id, NULL, 0);
    if (share_memory == NULL - 1) {
        debug_log("failed on shmat\n");
        goto finish2;
    }
    debug_log("allocate memory attached at %x and main entry %x\n", (int*)share_memory, (int)&main);
    share_stuff = (struct shared_use_st *)share_memory;
    share_stuff->written_by_you = 0;
    while (running) {
        while (share_stuff->written_by_you == 1) {
            sleep(1);
            debug_log("waiting for client\n");
        }
        debug_log("Enter some test: ");
        fgets(buffer, BUFSIZ, stdin);
        strncpy(share_stuff->some_text, buffer, TEXT_SZ);
        share_stuff->written_by_you = 1;
        if (strncmp(buffer, "end", 3) == 0) {
            running = 0;
        }
    }
    debug_log("finish.....\n");
finish2:
    if (shmdt(share_memory) == -1) {
        debug_log("failed on share_memory\n");
    }
    debug_log("finish test...\n");
    return ret;
}

$ ./test_shm_1.elf

$ ./test_shm_2.elf

image-20220401135958055

Ref

Footnotes

  1. Stackoverflow: understand-shmat-and-attachment-to-the-process-memory 2

  2. Linux Programmer's Manual - shmat, shmdt - System V shared memory operations

  3. Linux Programmer's Manual - shmget - allocates a System V shared memory segment 2

  4. Linux Programmer's Manual - shmctl - System V shared memory control 2

07_ARMv8_汇编器Using as

07_ARMv8_汇编器Using as

  • 关键字
  • 伪指令
  • 汇编宏

1 关键字

1.1 label 和 symbol

代表它所在的地址,也可以当做变量或者符号。

  • 全局symbol:.global xxxxx
  • 局部symbol (label):在函数内使用,开头以0-99直接的数字为标号名,通常和b指令结合使用。
  • f:指示编译器向前搜索
  • b:指示编译器向后搜索
.global my_sym

my_sym:
		mov x1, 0x80000
		mov x2, 0x200000
		add x3, x1, 32
1:
		ldr x4, [x1], #8
		str x4, [x2], #8
		cmp x1, x3
		b.cc 1b								# 向1 symbol后面的代码执行。
		
		ret

1.2 伪指令

对齐伪指令.align

声明align后面的汇编必须从下一个能被2^n整除的地址开始分配。ARM64系统中,第一个参数表示2^n大小。

Define: .align [abs-expr[, abs-expr[, abs-expr]]

.align 3						# 2^3 = 8 字节对齐
.global string1
string1:
		.string "Boot at EL"

string1的地址必须是能被8整除的。

数据定义伪指令

  • .byte 把8位数当做数据插入到汇编当中
  • .hword 把16位数当成数据插入到汇编当中
  • .long.int:把32位数当成数据插入到汇编当中
  • .quad:64位数
  • .float:浮点数
  • .ascii "string":把string当做数据插入到汇编当中,ascii未操作定义字符串需要自行插入‘\0’
  • .asciz "string": 不需要手动插入'\0'的string。

执行伪指令

  • .rept:重复执行伪指令
  • .equ.set:赋值操作 .equ symbol, expression
.rept 3
.long 0
.endr
# 等效于
.long 0
.long 0
.long 0
# 赋值例子
.equ data01, 100
.equ data02, 50

.global main
main:
	ldr x2, =data01
	ldr x3, =data02

函数相关伪指令

  • .gloabl:定义一个全局的符号,可以为变量可以为函数
  • .include:引用头文件
  • .if, .else, .endif:控制语句
  • .ifdef symbol:判断symbol是否定义
  • .ifndef symbol:未定义
  • .ifc string1, string2:判断两个字符串是否相等
  • .ifeq expression: 判断expression的值是否为0
  • .ifeqs string1, string2: 等同于ifc
  • .ifge expression:判断值是否≥0
  • .ifle expression:判断值是否≤0
  • .ifne expression:判断值是否不为0
运算符 说明
expr1 == expr2 若 expr1 等于 expr2,则返回“真”
expr1 != expr2 若 expr1 不等于 expr2,则返回“真”
expr1 > expr2 若 expr1 大于 expr2,则返回"真”
expr1 ≥ expr2 若 expr1 大于等于 expr2,则返回“真”
expr1 < expr2 若 expr1 小于 expr2,则返回“真”
expr1 ≤ expr2 若 expr1 小于等于 expr2,则返回“真”
!expr1 若 expr 为假,则返回“真”
expr1expr2 对 expr1 和 expr2 执行逻辑 AND 运算
expr1 || expr2 对 1xprl 和 expr2 执行逻辑 OR 运算
expr1 & expr2 对 expr1 和 expr2 执行按位 AND 运算
CARR1? 若进位标志位置 11则返回“真”
OVERFLOW ? 若溢出标志位置 1,则返回“真”
PARITY ? 若奇偶标志位置 1,则返回“真”
SIGN ? 若符号标志位置 1,则返回“真”
ZERO ? 若零标志位置 1,则返回“真”

段相关的伪指令

.section

.section name, "flag"

flag就是ELF文件中的adewxMSGT?可以查看文档1,我们在02_ELF文件结构_浅析内部文件结构2中描述了ELF的段的结构,可以参考。

  • b

    bss section (uninitialized data)

  • n

    section is not loaded

  • w

    writable section

  • d

    data section

  • r

    read-only section

  • x

    executable section

If no flags are specified, the default flags depend upon the section name. If the section name is not recognized, the default will be for the section to be loaded and writable.

If the optional argument to the .section directive is not quoted, it is taken as a subsegment number (see section Sub-Sections).

For ELF targets, the .section directive is used like this:

.section name[, "flags"[, @type]]

The optional flags argument is a quoted string which may contain any combintion of the following characters:

  • a

    section is allocatable

  • w

    section is writable

  • x

    section is executable

例子:把符号映射到.idmap.text段,属性为awx

.section ".idmap.text", "awx"
#-----------------------------------
.section .data
.algin 3
.global my_data0
.my_data0:
		.dword 0x00
#----------------------------------- mapping to .data section
.section .text
......
#----------------------------------- mapping to .text section
.pushsection

.pushsection:下面的代码push接到指定的section当中。

.popsection

.popsection:结束push

两个伪指令需要成对使用,仅仅是把pushsection和popsection的圈出来的代码复制链接到指定的section中,其他代码还在原来的section。

.section .text
.global my_add_func
my_add_func:
	...
	ret
	
.pushsection ".idmap.text", "awx"
.global my_sub_func
my_sub_func:
	.....
	ret
.popsection

....

这个例子就是把my_sub_func从 .text段里面提取出来然后链接到.idmap.text里面。

实验

【题目】

使用汇编的数据定义伪指令,可以实现表的定义,例如Linux内核使用.quad和.asciz定义了一个kallsyms的表,地址和函数名的对应关系:

  • 0x800800 -> func_a
  • 0x800860-> func_b
  • 0x800880-> func_c

请在汇编里定义一个类似的表,在C语言中根据函数的地址查找表,并且正确打印函数的名称。这个非常常用,我们在调试死机状态的时候可以根据这样的方法把函数的调用名称和栈指针打出来,方便我们调试。

.align 3
.global func_addr
func_addr:
	.qual 0x800800
	.qual 0x800860
	.qual 0x800880

.align 3
.global func_str
func_str:
	.asciz "func_a"
	.asciz "func_b"
	.asciz "func_c"

.align 3
.global func_num_syms
func_num_syms:
	.quad 3
extern unsigned long func_addr[];
extern char func_str[];
extern unsigned long func_num;
extern int add_f(int a, int b, int c);

static int print_func_name(unsigned long addr)
{
	int i = 0;
	char *p, *str;

	for (i = 0; i < func_num; i++) {
		if (addr == func_addr[i]) {
			goto found;
		}
	}
	uart_send_string("not found func\n");

found:
	p = (char *)&func_str;
	while(1) {
		p++;
		if (*p == '\0') {
			i--;
		}
		if (i == 0) {
			p++;
			str = p;
			uart_send_string(str);
			break;
		}
	}
	return 0;
}

2 汇编宏

2.1 定义

.macro macname macargs ...

.endm

示例1:

.macro add p1 p2
add x0, \p1, \p2
.endm

#可以配置初始值
.macro reserve_str p1=0, p2

#特殊字符(错误使用)
.macro opcode base length
\base.\length
.endm

.macro opcode base length
\base\().\length
.endm

在kernel里面有这样的代码:

.macro kernel_ventry, el, label, regsize = 64
.algin 7
sub sp, sp, #S_FRAME_SIZE
b el\()\el()_\label
.endm

最后结果是b el1_irq

2.2 实验

在汇编文件通过如下两个函数:

  • long add_1(a, b)
  • long add_2(a, b)

然后写一个宏定义

.macro add a, b, label

#这里调用add_1或者add_2函数,label等于1或者2

.endm

.align 3
add_1:
	add x0, x0, #1
	add x0, x0, x1
	ret

.align 3
add_2:
	add x0, x0, #2
	add x0, x0, x1
	ret

.global add_func
.macro add_func a, b, label
.align 3
	mov x0, \a
	mov x1, \b
	bl add_\()\label
.endm


macro1:
	mov x9, x30
	add_func x0, x1, 1
	mov x30, x9
	ret

macro2:
	mov x9, x30
	add_func x0, x1, 2
	mov x30, x9
	ret

.global add_f
.align 3
add_f:
	mov x9, x30
	cmp x2, 1
	b.eq macro1

	cmp x2, 2
	b.eq macro2

	mov x30, x9
	ret

Ref

Footnotes

  1. Using as - GNU Assembler, [.section name]

  2. 02_ELF文件结构_浅析内部文件结构

Linux-用户空间-多线程与同步

Linux用户空间多线程与同步

Linux用户空间的多线程和同步是一个非常重要的概念,即便在工程中能够利用这部分内容,但是还需要更系统化的对这部分进行了解,因为我们花一些时间来整理这部分的笔记。Linux用户空间的多线程为POSIX线程,与进程之间的区别就不再赘述了。多线程的优点在于比进程有着更小的开销,且共享信息更为便利,而带来的缺点难于同步、线程交互控制和调试

线程中引出一个可重入的概念,可重入 <reentrant, [ˌriˈɛntrənt]>,用于避免类似于fputs之类的函数,这些函数通常会有全局性的缓冲区存储数据。这也提示我们如果要编写一个reentrant的函数,对于修改全局性的缓冲存储数据,要格外的留心。另外,在编写reentrant的程序时,需要_REENTRANT宏定义来告诉编译器我们需要可重入的功能。reentrant还有另外一些知识点,在Linux中为了reentrant安全做了一些操作:

  • 函数后加_r作为可重入安全标识:gethostbyname, 可重入版本 gethostbyname_r。
  • 编译时候-D_REENTRANT会使用线程安全的函数。
  • errno.h中顶一个的变量errno,加入了线程安全的处理。

1. pthread接口与编译

1.1 pthread_create

API Define:

#include <pthread.h>

int pthread_create (pthread_t *thread,
                    pthread_attr_t * attr,
                    void *(*start_routine)(void*),
                    void *arg);

Parameters:

Params I/O Details
pthread_t *thread Input thread context
pthread_attr_t* attr Input thread attribute, if there is no attribute, the NULL shall be inputted.
void *(*start_routine)(void*) Input New thread entry.
void *arg Input arguments of new thread entry.

Return:

  • 0 : Success
  • otherwise:

1.2 pthread_exit

API Define:

#include <pthread.h>

void pthread_exit (void *retval);

Parameters:

Params I/O Details
void *retval Output 返回指向某个对象的指针,注意不可以返回一个指向局部变量的指针,局部变量在栈回溯后会消失。

Return:

  • None

1.3 pthread_join

API Define:

#include <pthread.h>

int pthread_join(pthread_t thread, 
                 void **retval);

Parameters:

Params I/O Details
pthread_t thread Input 指定要等待的线程
void **retval Output 返回指向某个对象的指针,注意不可以返回一个指向局部变量的指针,局部变量在栈回溯后会消失。

Return:

On success, pthread_join() returns 0; on error, it returns an error number.

1.4 用户空间的同步机制

1.4.1 基础的例子

请参考:test1_basic_thread on the link

这里需要注意编译选项:

  • -lpthread
  • -D_REENTRANT

对于第一个编译选项,如果系统默认是NPTL线程库,无需-lpthread-D_REENTRANT默认会使用线程安全的实现。

1.4.2 10个线程竞争

这里塑造一个测试场景,10个线程竞争各自输出自己数值,0线程输出0,1线程输出1,以此类推到9号线程输出到9。这里线程竞争不加任何的同步机制,让线程彼此自由运行。

注意,sprintf是线程不安全的。

请参考:test2_10_threads_no_sync on the link

test_thread.c:test2_10_threads_no_sync:168--create 10 threads
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000111111111111111111111111111111111111111111111111111111111111111111111343333333333333333333444431522222222555554333787872151test_thread.c:test2_10_threads_no_sync:179--created 10 treads.
4444test_thread.c:test2_10_threads_no_sync:182--5thread 0 exit (null)
852386133636333333333666666666666688886666666166666666666669799999997777676767777777777777777777777777777777777777777777779999999999343373777777598222222222226441111777332121111111111111111146311613888888888888888888888888888888888888888888888888888888888888888888888888888888888666666631222222222222222222222222222222222222222222222222222222222222222222222222222228888888888555555555555555577373763test_thread.c:test2_10_threads_no_sync:184--66666666666666666669433333359549555549494474544444444444755557773699496669696699799999999999999999999997thread 1 exit (null)
333937555test_thread.c:test2_10_threads_no_sync:186--thread 2 exit (null)
77797999999999999999999999999999999999996666699999666666666666666666674444445555555339999953337777744477773444433333333335444444447777777774444444444444444444444444444444444444555555555555533333333333333333333333444444444555555555555555555555555555555555555553test_thread.c:test2_10_threads_no_sync:188--thread 3 exit (null)
test_thread.c:test2_10_threads_no_sync:190--thread 4 exit (null)
test_thread.c:test2_10_threads_no_sync:192--thread 5 exit (null)
test_thread.c:test2_10_threads_no_sync:194--thread 6 exit (null)
test_thread.c:test2_10_threads_no_sync:196--thread 7 exit (null)
test_thread.c:test2_10_threads_no_sync:198--thread 8 exit (null)
test_thread.c:test2_10_threads_no_sync:200--thread 9 exit (null)
test_thread.c:test2_10_threads_no_sync:201--test 2 finish 
main : test done !!

线程执行的过程完全是随机的。

1.4.3 信号量semaphore

semaphore <ˈseməfɔːr>

#include <fcntl.h>           /* For O_* constants */
#include <sys/stat.h>        /* For mode constants */
#include <semaphore.h>

sem_t *sem_open(const char *name, int oflag);
sem_t *sem_open(const char *name, int oflag,
                mode_t mode, unsigned int value);
int sem_post(sem_t *sem);
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *restrict sem,
                  const struct timespec *restrict abs_timeout);
int sem_close(sem_t *sem);

信号量S是线程同步的重要手段,sem_post和sem_wait彼此之间配合,被称为PV操作。P使之增加,V使之减少。wait函数如果S为0的时候就要等待;如果S>0的时候,wait就跳过,并且进行S--。

注意,MACOS已经舍弃了sem_init和sem_destroy两个函数,需要使用sem_opensem_unlink

例子:

  • 设定一个共享的全局空间work_area,设定一个全局空间备份区域back_area
  • mainloop:等待用户输入字符串fgets,存储进入work_area
  • thread01: 打印用户的字符的个数,并在用户输入字符串尾部加!
  • thread02: 把thread01修改的字符串存储备份空间,并且work_area清零。

分析:

mainloop处理fgets字符串之后存入work_area,如果不加线程同步机制,thread01和thread02不断的竞争,最终达到稳态的时候,thread01写入1个!,thread02就会把他清掉。最后备份区域要么是0,要么是1个!。因此需要加同步机制,需要顺序是:

  • mainloop写入字符串,t01和t02全部等待
  • mainloop释放信号允许t01开始处理,需要让t02等待
  • t01处理完毕之后,释放信号允许t02处理
  • t02开始处理完,结束一个走起

因此,mainloop任务之后需要post一个信号量sem_01, 在t01的handler之后需要post一个信号量sem_02。t01 wait,mainloop的信号量;t02 wait t01的信号量。

请参考:test2_thread_sem.c on the link

1.4.4 spinlock和mutex

spinlock和mutex在功能上是一样的,关于不同往上已经很多的资料在阐述spinlock和mutex的不一样的。在Linux的user space,需要使用pthread中给定的spinlock和mutex,pthread_mutex_t和pthread_spinlock_t,在MACOS里面已经废弃掉了spinlock,可能考虑spinlock的忙等待十分的耗费CPU。我也做了相关的实验,塑造一种"死锁"场景,子线程不断的获取锁,分别看CPU的占用。实验结果和理论一致,spinlock会占用整个CPU的资源,使CPU根本无调度的窗口期。如果塑造两个spinlock的死锁,可以观察到两个CPU的调度资源被吃空,CPU使用率一度达到200%。这里可以总结一些spinlock和mutex的区别:

  • spinlock忙等待,吃掉所有cpu资源;mutex使线程进入sleep状态。mutex由此引发的一个问题就是,sleep和wakeup占用了系统的调度,并没有spinlock高效。我理解,可能在某些很重要的时序场合spinlock应该被考虑使用。spinlock处于忙等待,自然的CPU无法调度,不能被其他task打断。
  • 在Linux Userspace的pthread接口中,并没有给出spin_lock_irq等接口,在Linux内核中是有的。spinlock由于是忙等待,没有涉及调度,也没有涉及sleep,因此只要不屏蔽中断,是可以用于中断上下文的;而mutex涉及sleep,是不可以用于中断上下文的(内核进入中断之前会关闭系统的调度,一旦睡眠,内核调度无法响应别的进程,系统会hang死,中断服务程序一定不能sleep
  • semaphore和mutex一致,同样使用调度。

使用spinlock的死锁场景:

image-20220322101014839

使用mutex的死锁场景:

image-20220322101028140

在ARMv7架构(单核imx6ULL芯片测试)
WeChat Image_20220329114610
似乎CPU并没有跑满,原因暂时未知。

我觉得这个文献里面对比pthread_mutex和pthread_spinlock的调度性能对比可以参考一下1

这里面实验为:

1.4.5 线程的属性

在pthread建立的时候,pthread_create的第二个参数为pthread_attr_t,这里面可以设定线程的运行属性2

int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);

接着linux提供很多接口set和get属性:

  • detachstate: 脱离线程,该线程完成任务直接退出,不需要汇报主线程。可以配置下列宏定义

    • PTHREAD_CREATE_JOINABLE:默认配置
    • PTHREAD_CREATE_DETACHED:设定之后不需要pthread_join了
  • schdpolicy: 线程的调度方式

    • SCHED_OTHER: 默认配置

    • SCHED_RP:时间片流转, 当采用SHCED_RP策略的进程的时间片用完,系统将重新分配时间片,并置于就绪队列尾。放在队列尾保证了所有具有相同优先级的RR任务的调度公平。实时进程将得到优先调用,实时进程根据实时优先级决定调度权值,分时进程则通过nice和counter值决定权值,nice越小,counter越大,被调度的概率越大,也就是曾经使用了cpu最少的进程将会得到优先调度3

    • SCHED_FIFO:先到先服务。一旦占用cpu则一直运行。一直运行直到有 更高优先级任务到达或自己放弃。如果有相同优先级的实时进程(根据优先级计算的调度权值是一样的)已经准备好,FIFO时必须等待该进程主动放弃后才可以运行这个优先级相同的任务。而RR可以让每个任务都执行一段时间3需要设置实时优先级rt_priority(1-99)

    • 注意,设定调度策略之后,同样不需要设定pthread_join了

      int max_priority;
      int min_priority;
      struct sched_param sv;
      
      ret = pthread_attr_setschedpolicy(&thread_attr, SCHED_OTHER);
      
      max_priority = sched_get_priority_max(SCHED_OTHER);
      min_priority = sched_get_priority_min(SCHED_OTHER);
      sv.sched_priority = min_priority;
      ret = pthread_attr_setschedparam(&thread_attr, &sv);
  • schedparam: 和schedpolicy结合使用,上边的例子

  • inheritsched: 调度由属性明确地设置

    • PTHREAD_EXPLICIT_SCHED:默认,明确的设置
    • PTHREAD_INHERIT_SCHED:沿用创建者使用的参数
  • scope:线程调度的计算方式

    • PTHREAD_SCOPE_SYSTEM: 目前只有这一种取值
  • stacksize: 设置线程的栈大小,字节,定义_POSIX_THREAD_ATTR_STACKSIZE的时候才会支持该属性的设定。Linux目前支持的线程栈很大,所以这个功能对于linux来说有点多余。

1.4.6 取消线程

接口如下4

int pthread_cancel(pthread_t thread);
int pthread_setcancelstate(int state, int *oldstate);
int pthread_setcanceltype(int type, int *oldtype);

对于setcanceltype:

  • PTHREAD_CANCEL_ASYNCHRONOUS:立即取消线程
  • PTHREAD_CANCEL_DEFERRED:在接受取消消息请求后,一直等待知道线程执行下属函数之一才会取消
    • pthread_cond_timewait
    • pthread_testcancel
    • sem_wait
    • sigwait

1.4.7 条件变量

除了mutex和spinlock,在pthread层级接口还提供了条件变量,和mutex打配合完成更高效的线程与线程之间的同步。比如这样的场景,小明爸爸每隔三天会向桌子上放一个苹果,小明可以一天吃一个苹果,小明不知道爸爸什么时候来放苹果,所以小明的策略是写作业写一个小时就去桌子上查看一下是不是有苹果,这样效率就十分低。条件变量就可以提供一个通知机制,小明爸爸放苹果之后,通知小明去取,小明再去桌子拿苹果吃,这样显著提高效率。

  • 小明爸爸可以使用pthread_cond_signal或者pthread_cond_boardcast来通知小明
  • 小明可以持有pthread_cond_wait来等待爸爸的通知。
int pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t *cond_attr);
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,const timespec *abstime);
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);

这里说下signal和boardcast的区别,大刚有两个孩子,小明和小红,大刚三天放2个苹果,小红和小明一次拿一个,并且小明和小红使用cond_wait等待着大刚的cond_signal。如果大刚使用cond_signal,那么一次只能通知一个人,而且是轮着来,这样就非常没有效率。但如果大刚使用cond_boardcast,那么小明和小红可以同时收到消息。

关于条件变量的测试:https://github.com/carloscn/clab/blob/master/linux/test_thread/test_thread_cond.c

这里创建了一个生产者和两个消费者,竞争cond状态。

2 Topics

我相信,关于多线程会有很多TOPIC值的探讨,这里遇到什么TOPIC都会整理下来。

2.1 pthread使用多核

之前我以为多核调度是进程层面的事情,也很好奇,后来查了多方材料,原来pthread在内核层面是支持多核心执行的,具体调度我给Linux内核的学习留白,但在Linux userspace,pthread是提供配置多核运行和获取在哪个核心的接口5

#define _GNU_SOURCE             /* See feature_test_macros(7) */
#include <pthread.h>
int pthread_setaffinity_np(pthread_t thread, size_t cpusetsize,
                           const cpu_set_t *cpuset);
int pthread_getaffinity_np(pthread_t thread, size_t cpusetsize,
                           cpu_set_t *cpuset);

这里也有相应的example来测试线程在哪个核心上面,配置8个核心,会根据实际配置核心输出。

  • 注意:_GNU_SOURCE务必define,否则CPU_SET_SIZE这些宏定义找不到
#define _GNU_SOURCE
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#define handle_error_en(en, msg) \
        do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0)
int
main(int argc, char *argv[])
{
    int s;
    cpu_set_t cpuset;
    pthread_t thread;
    thread = pthread_self();
    /* Set affinity mask to include CPUs 0 to 7. */
    CPU_ZERO(&cpuset);
    for (int j = 0; j < 8; j++)
        CPU_SET(j, &cpuset);
    s = pthread_setaffinity_np(thread, sizeof(cpuset), &cpuset);
    if (s != 0)
        handle_error_en(s, "pthread_setaffinity_np");
    /* Check the actual affinity mask assigned to the thread. */
    s = pthread_getaffinity_np(thread, sizeof(cpuset), &cpuset);
    if (s != 0)
        handle_error_en(s, "pthread_getaffinity_np");
    printf("Set returned by pthread_getaffinity_np() contained:\n");
    for (int j = 0; j < CPU_SETSIZE; j++)
        if (CPU_ISSET(j, &cpuset))
            printf("    CPU %d\n", j);
    exit(EXIT_SUCCESS);
}

我分别在ubuntu虚拟机和armv7(cortex-A7单核)上面测试得到不一样的结论:

image-20220331220717666

  • 在ubuntu虚拟机上面这里面调度了两个core
  • 在armv7a(cortex-A7单核)上面测试只有一个核心

Ref

Footnotes

  1. Pthread_Mutex_t Vs Pthread_Spinlock_t (转载

  2. archlinux-man-page-pthread-attr-init

  3. inux进程/线程调度策略(SCHED_OTHER,SCHED_FIFO,SCHED_RR) 2

  4. archlinux-man-page-pthread-cancel

  5. archlinux-man-pthread_setaffinity_np

Qt_QWT编译与配置-Windows/Linux环境

QWT编译与配置-Windows/Linux环境

QWT和FFTW两种开源组件是常用的工程软件支持组件,QWT可以提供丰富的绘图组件功能,FFTW是优秀数字波形分析软件。本文使用基于LGPL版权协议的开源项目QWT的源代码和FFTW在Window 64bit/Linux环境下的Qt上进行编译,编译器使用MinGW-64bit版本。最终配置生成QWT的静态库文件和将qwt ui组件集成到QtCreator的Designer中。

QWT的源代码下载地址目录:https://sourceforge.net/projects/qwt/files/qwt/

请下载zip版本的源代码(我这里下载的是qwt-6.1.4.zip文件)

1 Windows环境

本文使用Window环境为:

系统: Windows 10 64bits

QtCreator版本: Qt5.12.1 / MinGW 64版本

a)解压qwt-6.1.4.zip文件到本地路径

解压qwt-6.1.4文件会发现该文件是一个基于Qt的工程文件,使用本地的QtCreator自帶的qt 命令行工具,分别编译release版本。

b) 切换到解压路径

C) 进行编译

c.1 更改配置文件

win32 {
    QWT_INSTALL_PREFIX    = C:/Qwt-$$QWT_VERSION
    # QWT_INSTALL_PREFIX = C:/Qwt-$$QWT_VERSION-qt-$$QT_VERSION
}

qmake qwt.pro

mingw32-make -j8

mingw32-make install

d) 配置QWT工作(关闭QtCreator)

拷贝工作:
d.1) 从 C:\Qwt-6.1.4\lib下拷贝libqwt.a和libqwtd.a 到 【qt安装路径\Qt5.12.1\5.12.1\mingw73_64\lib】文件夹下 。
d.2) 从 C:\Qwt-6.1.4\lib下拷贝qwt.dll和qwtd.dll 到 【qt安装路径\Qt5.12.1\5.12.1\mingw73_64\bin】文件夹下
d.3) 从 C:\Qwt-6.1.4\plugins\designer下拷贝qwt_designer_plugin.dll 到 【qt安装路径\Qt5.12.1\5.12.1\mingw73_64\plugins\designer】文件夹下 。

d.4) 在 【qt安装路径\Qt5.12.1\5.12.1\mingw73_64\include】文件夹下新建Qwt文件夹,并将C:\Qwt-6.1.4\include此文件夹下所有内容拷贝到刚才新建的Qwt文件夹内。

over#

2 Linux環境

本文使用的Linux環境爲:

系統: Manjaro KDE 64 bit版本gcc version 8.2.1 20181127 (GCC)

Qt版本: Qt5.12.1

a) qwt的編譯

a) 使用unzip命令解压qwt-6.1.4文件会发现该文件是一个基于Qt的工程文件,使用Linux的終端命令對QWT進行編譯。

b) 切換到安裝目錄,檢查qwtconfig.pri文件,查看最終make install 路徑是否是你想要設定的路徑,我這裏使用默認的路徑。

c) qmake qwt.pro

d) make -j8

e) sudo make install 如果路徑是root下的,則需要使用sudo。

最終qwt會在/usr/local/qwt-6.1.4創建所有的開發用的文件。

b) qwt的配置

b.1) sudo cp /usr/local/qwt-6.1.4/lib/* qt安裝路徑/Qt5.12.1/5.12.1/gcc_64/lib

b.2) 在qt安裝路徑/Qt5.12.1/5.12.1/gcc_64/include 創建Qwt文件夾mkdir Qwt

sudo cp /usr/local/qwt-6.1.4/include/* qt安裝路徑/Qt5.12.1/5.12.1/gcc_64/include/Qwt

b.3) sudo cp /usr/local/qwt-6.1.4/plugins/designer/libqwt_designer_plugin.so qt安裝路徑/Qt5.12.1/5.12.1/gcc_64/plugins/designer/

完成designer的組件支持。

c) 配置環境變量

sudo vim /etc/profile

在尾部追加:

export LD_LIBRARY_PATH=/usr/local/qwt-6.1.4/lib:$LD_LIBRARY_PATH
export CPLUS_INCLUDE_PATH=/usr/local/qwt-6.1.4/include:$CPLUS_INCLUDE_PATH
export C_INCLUDE_PATH=/usr/local/qwt-6.1.4/include:$C_INCLUDE_PATH

over#

Qt_Linux编译Qt4的环境_OMAPL138

Linux编译Qt4的环境_OMAPL138

手里有一块创龙OMAPL138的板子,我要在上面成功移植Qt环境和触摸屏幕,这是我第二次进行Linux的Qt环境移植,发现了很多问题,需要重新整理。 我编译了,Qt5版本以上的,结果就是不成功,总是死在PDA问题上,在 configure文件上加入-xcb的选项,就算我安装了xcb所有的库文件,最后还是出问题,我还在研究之中,等着编译Qt5通过之后,我会重新写一个Linux编译Qt5的环境。

本文不适合配置Qt5的环境,不要用在Qt5上

准备

  • 交叉编译环境(一定要找到适合你板子的交叉编译环境)

  • Qte嵌入式源代码,文件的名字如同:qt-everywhere-opensource-src-4.8.6.tar.xz


我的环境

  • PC 机: Ubuntu16.04 (64bit)

  • OMAPL138提供的交叉编译工具链:arm-arago-linux-gnueabi http://www.veryarm.com/arm-none-linux-gnueabi-gcc

    • PS:不同平台的交叉编译工具链不同,基本上芯片厂商出一个ARM芯片,就配套一个交叉编译工具链。
    • PS:最好检测板子是否支持整个工具链的方式就是编写一个最简单的hello word程序,然后到目标板子上运行,能够运行出来就说明这个交叉编译环境对劲儿。
  • Qt源码: qt-everywhere-opensource-src-4.8.6.tar.gz

    *下载地址:https://mirrors.tuna.tsinghua.edu.cn/qt/official_releases/qt/ 进入目录./4.8/4.8.6/single 找到这个文件。

  • tslib 1.4 触摸屏幕支持库:tslib1.4

交叉编译环境配置

其实也可以不进行配置,反正后面我们在编译器名称的时候都用绝对路径

在本文中我就直接写交叉编译环境的路径按照我电脑上的配置了,如果配置你的话注意灵活修改路径:

我的放在了:/home/delvis/opt/toolschain/omapl138 文件夹中

编译tslib1.4

对触摸屏信号的获取、校正、滤波处理,均采用开源的tslib,本文采用的tslib版本为最新的tslib1.4(可以从本文提供的链接中下载tslib1.4)。
1.将下载好的tslib1.4拷贝到/home/lz/transplant目录下(可以根据自己的系统选择某一目录),然后执行解压缩命令

tar -vxf tslib-1.4.tar.gz

切换到tslib目录:

cd tslib

安装交叉编译tslib必须的一些工具(可以先查看是否已安装,ubuntu16.04自带这些工具,可跳过)

sudo apt-get install autoconf
sudo apt-get install automake
sudo apt-get install libtool

2.利用脚本写编译过程
在tslib文件夹下新建文件configTslib14.sh

touch autoconfig.sh
vim autoconfig.sh
chmod 777 autoconfig.sh

内容如下:

#!/bin/sh
make clean && make distclean

echo "ac_cv_func_malloc_0_nonnull=yes" >arm-linux.cache

CC=/home/delvis/opt/toolschain/omapl138/arm-none-linux-gnueabi

./configure --host=arm-linux --prefix=/home/delvis/opt/tslib1.4 --cache-file=arm-linux.cache

make && make install

这里面需要注意的地方就是:

  • CC=/home/delvis/opt/toolschain/omapl138/arm-none-linux-gnueabi 这个位置一定要修改成你的交叉编译环境的路径。
  • --prefix=/home/delvis/opt/tslib1.4 这个是编译好输出的路径,这个位置要注意了,很多网上的教程输出到的/opt/tslib1.4 这个路径,但是这个/opt的文件路径需要的权限是root,而我们编译运行的时候的路径没有使用root用户或者没有加sudo,所以这里推荐不要直接输出到/opt文件夹,而是自己的用户文件夹。

然后运行

./autoconfig.sh

执行结束后,我们查看一下是否安装成功,执行命令:

ls /home/delvis/opt/tslib1.4

如果出现bin,etc,include,lib这4个目录,如下图所示,说明交叉编译并安装tslib成功。 这个先放在这里了。这里写图片描述

然后开始我们的重头戏,交叉编译Qt

交叉编译QT4.8.6

交叉编译Qt成功之后,编译后的文件需要应用两个位置,一个部分需要移植到我们的开发板上,另一个部分需要把编译成功的库配置到我们运行QtCreator的PC机上,两个部分只有统一了才不会出问题。

1)准备好Qt的源码文件

将下载的qt-everywhere-opensource-src-4.8.6.tar.gz执行如下命令解压:

tar -vxf qt-everywhere-opensource-src-4.8.6.tar.gz
cd qt-everywhere-opensource-src-4.8.6

2)配置源码文件

创建架构信息:

  • 进入架构信息路径 (qt4的源码路径一般是)

cd ./qtbase/mkspecs/qws

  • 创建一个文件夹(ps: 预告以下这个文件名会影响到后面的配置,这个东西我搞了很久

mkdir omapl-g++

  • 进入这个文件夹并创建两个文件

cd omapl-g++; touch qmake.config qplatformdefs.h

  • 修改qplatformdefs.h ,直接把下面这个粘贴上去,只有这一句。
#include "../../linux-g++/qplatformdefs.h"

如果发现在编译过程中提示找不到qplatformdefs.h整个文件,就在mkspec/qws文件夹的其他架构信息的文件夹,看看人家的怎么写的是什么路径,基本上就是你的路径。

  • 修改交叉编译架构用到的信息:
gedit qmake.conf

在qmake.conf文件中需要注意以下信息:

  • QMAKE_CFLAGS_RELEASE += O2 -march=xxxxx,就是整个march要注意自己的平台是什么架构的,比如说arm9基本上就是arm5te,cortexA8都有不一样的架构,整个地方必须写正确,否则当我们搭建好交叉编译环境编译Qt工程然后运行的时候,就会出现illeagle instruction. 的错误提示。也有些配置文件没有指定这个信息,最好也写上。

  • 我在这里的时候,即便是配置了交叉编译环境,还是提示arm-linux-gnueabi-gcc找不到,而我单拿出来在命令行上面 arm-linux-gnueabi-gcc -v 的时候还能显示出来,我换了四种方法配置交叉编译环境,重启N次还是不见效,所以我在qmake.config文件上做了手脚,使用绝对路径进行配置,没想到编译通过了,所以我建议在qmake.config文件上使用绝对路径。

  • 后面的include也是,看看mkspec/qws/中的寻文件的路径是什么,可能是前一层的目录,也可能是前两层的目录。

#
# qmake configuration for building with arm-linux-gnueabi-g++
#


MAKEFILE_GENERATOR      = UNIX
CONFIG                 += incremental
QMAKE_INCREMENTAL_STYLE = sublib

QT_QPA_DEFAULT_PLATFORM = linuxfb #eglfs

#Compiler Flags to take advantage of the ARM architecture
#Run the linux terminal command "arch" to see the detail your target cpu arch information.
QMAKE_CFLAGS_RELEASE += -O3 -march=armv5te
QMAKE_CXXFLAGS_RELEASE += -O3 -march=armv5te

include(../../common/g++.conf)
include(../../common/linux.conf)
include(../../common/qws.conf)

# modifications to g++.conf
QMAKE_CC                = /home/delvis/opt/toolschain/omapl/arm-arago-linux-gnueabi/bin/arm-arago-linux-gnueabi-gcc
QMAKE_CXX               = /home/delvis/opt/toolschain/omapl/arm-arago-linux-gnueabi/bin/arm-arago-linux-gnueabi-g++
QMAKE_LINK              = /home/delvis/opt/toolschain/omapl/arm-arago-linux-gnueabi/bin/arm-arago-linux-gnueabi-g++
QMAKE_LINK_SHLIB        = /home/delvis/opt/toolschain/omapl/arm-arago-linux-gnueabi/bin/arm-arago-linux-gnueabi-g++

# modifications to linux.conf
QMAKE_AR                = /home/delvis/opt/toolschain/omapl/arm-arago-linux-gnueabi/bin/arm-arago-linux-gnueabi-ar cqs
QMAKE_OBJCOPY           = /home/delvis/opt/toolschain/omapl/arm-arago-linux-gnueabi/bin/arm-arago-linux-gnueabi-objcopy
QMAKE_NM                = /home/delvis/opt/toolschain/omapl/arm-arago-linux-gnueabi/bin/arm-arago-linux-gnueabi-nm -P
QMAKE_STRIP             = /home/delvis/opt/toolschain/omapl/arm-arago-linux-gnueabi/bin/arm-arago-linux-gnueabi-strip
load(qt_config)

3)创建configrue的执行文件

回到qt的顶层目录,创建一个脚本文件,用于生成Makefile,执行命令

gedit run.sh
chmod 777 run.sh

输入下面内容并保存:

./configure -prefix /home/delvis/work/qt-arm-4.8.6 -embedded arm -xplatform qws/omapl138-g++ -no-webkit -qt-libtiff -qt-libmng -no-neon -qtlibinfix E -nomake examples -lrt

其中

-prefix /home/delvis/work/qt-arm-4.8.6代表我们编译完QT后要安装地址(这个文件需要用到两个地方);你可以根据自己需求进行配置编译,去掉不需要的部分,留下需要的部分,比如:增加-tslib代表QT对触摸板的支持,-I 和 -L后面分别为为第一步编译tslib的include和lib的安装目录。 (这里暂时不给这些)

执行命令:(这里推荐用sudo,管理员权限进行配置和编译,很多小伙伴遇到工具链中有一个东西没有调用的权限,结果搞了好久才搞明白,所以这里为了防止发生这样的问题,我们直接使用sudo

sudo ./run.sh

上述命令自动生成Makefile文件。

*)说几个坑

  • 在编译之前需要检查一下Qt的编译环境有没有配置好,主要还是一下几个方面:
  1. 在命令行输入: export
  2. 然后输出几个找到QMAKESPEC这个,如果有这个,我们就要把它删除掉,否则就会很蛋疼。我们使用unset QMAKESPEC 这个命令,把这个环境变量在当前的命令窗口下删除,这样就不影响我们编译了。
  • 还有一个比较重点的坑,就是我们上面提到的mkdir omapl-g++,如果我们对Qt源文件下的配置文件不加修改,那么就会报错:undefined reference to `QInotifyFileSystemWatcherEngine::create()

    查找相关资料,查到这个解决方法,看画红圈的位置,还有其他网站提供的解决方案,加入arm-linux字符,但是在我这里依然不好使。后来我想明白了,经过实验也是正确的,这些解决方法没有给定原理在哪里。如图:

    我们在mkspec/qws中创建了文件夹(架构信息)的名字叫做omapl-g++,所以我们这个位置需要改的信息应符合我们这个omapl-g++这个文件夹的名字

    在Qt根目录下:vim ./src/corelib/io/io.pri

    找到linux-*这一块,然后改成inux-*|omap*{

    这个意思就非常明显了,我们在mkspec/qws创建的文件夹的名字叫做omapl-g++ 那么需要在这里制定信息也是要匹配这个名字的,所以增加omap*这样的通配符,如果你是在mkspec/qws中创建的是zynq-g++ 那么显而易见那么增加的应该是zynq*或者zynq-g*都行。

4)开始编译

执行命令启动编译:(8线程编译,如果你的CPU受不住8线程你可以用双线程 -j2参数,-几就是几线程编译)

sudo make -j4

我的大概能编译10分钟左右,此时CPU利用率达到100%,温度如果是笔记本的话,超级高。编译时间根据你的配置多少来决定。编译的过程不可能是一帆风顺的,总会出现各种各样的错误,如果出现编译错误,那么就去百度解决。
编译结束后,执行安装命令:

sudo make install

我们切换到-prefix设定的目标目录下看看是否安装成功:

cd /home/delvis/work/qt-arm-4.8.6
ls

如果在bin目录出现了qmake基本上就成功了。

配置PC的QtCreator

这个部分不在赘述,我找到http://blog.csdn.net/u012175418/article/details/52704734 引用这个博客的“设置QT的交叉编译环境”,部分,按照这个步骤在PC机的Qt上面配置好我们刚编译的组件和刚刚编译Qt的交叉编译环境。

拷贝文件到目标板并配置环境

1)传输Qt4.8.6到开发板

就是把我们刚才编译的环境拷贝到我们的目标板的目录下,可以使用FTP,可以使用OTG挂在U盘的方式,我喜欢使用SSH协议的scp命令。

sudo scp -r /home/delvis/work/qt-arm-4.8.6 [email protected]:/opt 输入密码之后就可以把整个文件夹传输到开发板上面了。

2) 配置开发板的环境

我们传输完毕之后,则需要在开发板上设定环境,当我们运行qt程序的时候才能寻找到这些Qt库文件。

打开

vi /etc/profile

增加以下,需要注意的是QT_ROOT,写对Qt的路径,还有TSLIB_ROOT编译的是tslib的路径。

export TSLIB_ROOT=/opt/tslib1.4                                                 
export QT_ROOT=/opt/qt-arm-4.8.6                                                
export TSLIB_TSDEVICE=/dev/input/event2                                         
export TSLIB_TSEVENTTYPE=input                                                  
export TSLIB_CONFFILE=/opt/tslib1.4/etc/ts.conf                                 
export TSLIB_PLUGINDIR=/opt/tslib1.4/lib/ts                                     
export TSLIB_CONSOLEDEVICE=none                                                 
export TSLIB_FBDEVICE=/dev/fb0                                                  
export QWS_MOUSE_PROTO=tslib:/dev/input/event2                                  
export LD_LIBRARY_PATH=/lib:/usr/lib:/usr/local/lib:$QT_ROOT/lib:$TSLIB_ROOT/lib
export QT_QPA_PLATFORM_PLUGIN_PATH=$QT_ROOT/plugins
export QT_QPA_PLATFORM=linuxfb:tty=/dev/fb0
export QT_QPA_FONTDIR=$QT_ROOT/lib/fonts
export QT_QPA_GENERIC_PLUGINS=tslib 

完成之后,更新一下环境。

source /etc/profile

完成配置。

3)测试

我们利用Qt Creator制作一个简单的界面,然后编译出可执行文件,讲可执行文件传输到目标板子上面执行,我们的可执行程序叫做TestEM,需要qws参数。

./TestEM -qws

然后就可以看见界面了。

界面文件

参考文献:

[1] 德州仪器手册. Building_Qt. 德州仪器WIKI.

[2] Qt官网forum. Error in cross compilation for ARM OMAP35x development kit). Qt官方Forum.

[3] BigSam78. ARM 指令集版本和ARM 版本. 新浪博客.


版权声明:

1. 本文为MULTIBEANS团队研发跟随文章,未经允许不得转载。

2· 文中涉及的内容若有侵权行为,请与本人联系,本人会及时删除。

3· 尊重成果,本文将用的参考文献全部给出,向无私的工程师,爱好者致敬。

Qt_Linux编译移植Qt5的环境_Xillinx的ZYNQ平台

Linux编译Qt环境

2017年的十一假期,足不出户,一个人在教研室里面搞Qt的移植。我手里面有Samsung的CortexA8,Samsung的 CortexA53还有Ti的Sitara系列的AM3517的ARM,都成功的移植了Qt,然而在我接触ZYNQ这个平台的时候,偏偏搞的我三天的时间,无法移植,ZYNQ上面安装的是Linaro这个阉割版本的Ubuntu,怎么都不成功,一个问题,在我的PC机上编译完到Linaro上面运行的时候,没有反应,不启动也不报错。

准备

  • 交叉编译环境(一定要找到适合你板子的交叉编译环境)

你需要用嵌入式环境编译Qt和Qt所开发的程序。

  • Qte嵌入式源代码,文件的名字如同:qt-everywhere-opensource-src-5.7.0.tar.xz

我的环境:

PC 机: Ubuntu16.04 (64bit)

Linaro交叉编译工具链 : (gcc-linaro-arm-linux-gnueabihf-4.9-2014.07_linux.tar.xz)
Qt源码: qt-everywhere-opensource-src-5.7.0.tar.gz

交叉编译环境配置

其实也可以不进行配置,反正后面我们在编译器名称的时候都用绝对路径

配置环境变量一共有好几种方法,交叉编译环境的方法就不说了:

参考:http://www.linuxidc.com/Linux/2013-06/85902.htm

编译tslib1.4

对触摸屏信号的获取、校正、滤波处理,均采用开源的tslib,本文采用的tslib版本为最新的tslib1.4(可以从本文提供的链接中下载tslib1.4)。
1.将下载好的tslib1.4拷贝到/home/lz/transplant目录下(可以根据自己的系统选择某一目录),然后执行解压缩命令

tar -vxf tslib-1.4.tar.gz1

切换到tslib目录:

cd tslib

安装交叉编译tslib必须的一些工具(可以先查看是否已安装,ubuntu16.04自带这些工具,可跳过)

sudo apt-get install autoconf
sudo apt-get install automake
sudo apt-get install libtool

2.利用脚本写编译过程
在tslib文件夹下新建文件configTslib14.sh

vi configTslib14.sh

内容如下:

#!/bin/sh
make clean && make distclean
echo "ac_cv_func_malloc_0_nonnull=yes" >arm-linux.cache
CC=/usr/local/arm/arm-2014.05/bin/arm-none-linux-gnueabi-gcc ./configure --host=arm-linux --prefix=/opt/tslib1.4 --cache-file=arm-linux.cache
make && make install

然后运行configTslib14.sh

./configTslib14.sh

执行结束后,我们查看一下是否安装成功,执行命令:

ls /opt/tslib1.4

如果出现bin,etc,include,lib这4个目录,如下图所示,说明交叉编译并安装tslib成功。
这里写图片描述

*交叉编译QT5.7.0

将下载的qt-everywhere-opensource-src-5.7.0.tar.gz执行如下命令解压:

tar -vxf qt-everywhere-opensource-src-5.7.0.tar.gz
cd qt-everywhere-opensource-src-5.7.01

创建架构信息:

  • 进入架构信息路径

cd ./qtbase/mkspecs/

  • 创建两个文件

touch qmake.config qplatformdefs.h

  • 修改qplatformdefs.h ,直接把下面这个粘贴上去,对一个问
/****************************************************************************
**
** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies).
** Contact: http://www.qt-project.org/legal
** 
**
** $QT_BEGIN_LICENSE:LGPL21$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and Digia. For licensing terms and
** conditions see http://qt.digia.com/licensing. For further information
** use the contact form at http://qt.digia.com/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 2.1 or version 3 as published by the Free
** Software Foundation and appearing in the file LICENSE.LGPLv21 and
** LICENSE.LGPLv3 included in the packaging of this file. Please review the
** following information to ensure the GNU Lesser General Public License
** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** In addition, as a special exception, Digia gives you certain additional
** rights. These rights are described in the Digia Qt LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include "../linux-g++/qplatformdefs.h"
  • 修改交叉编译架构用到的信息:
gedit ./qtbase/mkspecs/linux-arm-gnueabi-g++/qmake.config

修改如下图所示:

我在这里的时候,即便是配置了交叉编译环境,还是提示arm-linux-gnueabi-gcc找不到,而我单拿出来在命令行上面 arm-linux-gnueabi-gcc -v 的时候还能显示出来,我换了四种方法配置交叉编译环境,重启N次还是不见效,所以我在qmake.config文件上做了手脚,使用绝对路径进行配置,没想到编译通过了,所以我建议在qmake.config文件上使用绝对路径。

#
# qmake configuration for building with arm-linux-gnueabi-g++
#

MAKEFILE_GENERATOR      = UNIX
CONFIG                 += incremental
QMAKE_INCREMENTAL_STYLE = sublib

QT_QPA_DEFAULT_PLATFORM = linux #eglfs
QMAKE_CFLAGS_RELEASE += -O2 -march=armv7-a
QMAKE_CXXFLAGS_RELEASE += -O2 -march=armv7-a

include(../common/linux.conf)
include(../common/gcc-base-unix.conf)
include(../common/g++-unix.conf)

# modifications to g++.conf
QMAKE_CC                = /home/delvis/opt/gcc-linaro-arm-linux-guneabihf/bin/arm-none-linux-gnueabi-gcc
QMAKE_CXX               = /home/delvis/opt/gcc-linaro-arm-linux-guneabihf/bin/arm-none-linux-gnueabi-g++
QMAKE_LINK              = /home/delvis/opt/gcc-linaro-arm-linux-guneabihf/bin/arm-none-linux-gnueabi-g++
QMAKE_LINK_SHLIB        = /home/delvis/opt/gcc-linaro-arm-linux-guneabihf/bin/arm-none-linux-gnueabi-g++

# modifications to linux.conf
QMAKE_AR                = /home/delvis/opt/gcc-linaro-arm-linux-guneabihf/bin/arm-none-linux-gnueabi-ar cqs
QMAKE_OBJCOPY           = /home/delvis/opt/gcc-linaro-arm-linux-guneabihf/bin/arm-none-linux-gnueabi-objcopy
QMAKE_NM                = /home/delvis/opt/gcc-linaro-arm-linux-guneabihf/bin/arm-none-linux-gnueabi-nm -P
QMAKE_STRIP             = /home/delvis/opt/gcc-linaro-arm-linux-guneabihf/bin/arm-none-linux-gnueabi-strip
load(qt_config)

创建一个脚本文件,用于生成Makefile,执行命令

gedit autoConfigure.sh

输入下面内容并保存:

./configure -release -opensource -xplatform linux-arm-gnueabi-g++ -prefix /opt/qt5-arm -no-c++11 -no-opengl

其中-prefix /opt/qt-5.7.0代表我们编译完QT5.4.1后要安装地址;-tslib代表QT对触摸板的支持,-I 和 -L后面分别为为第一步编译tslib的include和lib的安装目录。
执行命令:

chmod 777 qt.configure.sh
./autoConfigure.sh

上述命令自动生成Makefile文件。
执行命令启动编译:

make -j8

编译大概20分钟左右(I5-7500 的CPU)。
编译结束后,执行安装命令:

sudo make install

我们切换到目标目录下看看是否安装成功:

cd /opt/qt-5.7.0
ls

如图所示:

将/opt/qt-5.7.0和/opt/tslib1.4 拷贝到开发板的文件系统中对应的目录中。

Linux编译移植Qt5的环境_OMAPL138平台

Linux编译移植Qt5_OMAPL138

【导语】:昨天编译Qt5各种失败,各种离奇的错误在Google上面也搜索不到,真是让人“蛋疼菊紧”,今天把所有的环境全部清理干净,也重新解压了Qt5.1.1的源码包,重新走了一遍,效果还可以,也没有出现PDA LTS什么库问题,现在整理一下详细过程和细节点。

另外说一下,我使用Qt5.8.0的源码包进行编译,我的OMAPL138的ARM9架构,在configure阶段就提示我your platform arch too old. 意思就是我的平台的架构太老了,所以如果在ARM9的架构上面编译Qt5,选择一个稍微老一点的版本。我这里选择Qt5.1.1

**再补充一点:如果在曾经执行过./configure这个命令了,如果出了问题进行修正,最好把这个源码包删除,重新解压一个源码包,说不定哪个配置属性就影响了后续的操作,我之前就是总在这个里,以为每次./configure都是重新配置,然而并不是这样。 **


编译Qt5.1.1

1 准备工作

2 配置源码文件

下载qt-everywhere-opensource-src-5.1.1.tar.gz的解压,并且切换到这个目录。开始配置信息,我这里有配置好的文件,(链接: https://pan.baidu.com/s/1qYPyAoW 密码: 8eey)这个文件包含一个配置信息的run.sh脚本,和linux-arago-gnueabi-g++的架构信息文件夹。

把linux-arago-gnueabi-g++架构文件夹拷贝到./qtbase/mkspec文件夹里面,然后我们修改这个文件夹里面的qmake.conf文件,在程序内标定注意事项:

  • QMAKE_CC等编译器使用的是绝对路径。
  • QMAKE_CFLAGS_RELEASE 这里需要写好你目标板子的架构信息(可以在终端运行 arch命令查看)
  • include路径看看其他架构文件夹里面的qmake.conf如何指定,可能不同版本稍有不同。
#
# qmake configuration for building with arm-arago-linux-gnueabi-g++
#

MAKEFILE_GENERATOR      = UNIX
CONFIG                 += incremental
QMAKE_INCREMENTAL_STYLE = sublib

#Compiler Flags to take advantage of the ARM architecture
#Run the linux terminal command "arch" to see the detail your target cpu arch information.
QMAKE_CFLAGS_RELEASE += -O3 -march=armv5te
QMAKE_CXXFLAGS_RELEASE += -O3 -march=armv5te

include(../common/linux.conf)
include(../common/gcc-base-unix.conf)
include(../common/g++-unix.conf)

# modifications to g++.conf
QMAKE_CC                = /home/delvis/opt/toolschain/omapl/arm-arago-linux-gnueabi/bin/arm-arago-linux-gnueabi-gcc
QMAKE_CXX               = /home/delvis/opt/toolschain/omapl/arm-arago-linux-gnueabi/bin/arm-arago-linux-gnueabi-g++
QMAKE_LINK              = /home/delvis/opt/toolschain/omapl/arm-arago-linux-gnueabi/bin/arm-arago-linux-gnueabi-g++
QMAKE_LINK_SHLIB        = /home/delvis/opt/toolschain/omapl/arm-arago-linux-gnueabi/bin/arm-arago-linux-gnueabi-g++

# modifications to linux.conf
QMAKE_AR                = /home/delvis/opt/toolschain/omapl/arm-arago-linux-gnueabi/bin/arm-arago-linux-gnueabi-ar cqs
QMAKE_OBJCOPY           = /home/delvis/opt/toolschain/omapl/arm-arago-linux-gnueabi/bin/arm-arago-linux-gnueabi-objcopy
QMAKE_NM                = /home/delvis/opt/toolschain/omapl/arm-arago-linux-gnueabi/bin/arm-arago-linux-gnueabi-nm -P
QMAKE_STRIP             = /home/delvis/opt/toolschain/omapl/arm-arago-linux-gnueabi/bin/arm-arago-linux-gnueabi-strip
load(qt_config)

再来看run.sh的配置信息(注意给定run.sh chmod 777 权限)注意几个关键点:

  • -prefix :指定make install之后的安装路径。
  • -xplatform :注意是开头字母是x 还有platform,如果少了x会报错说:/usr/bin/ld: .libs/dev2gif.o: Relocations in generic ELF (EM: 40)
  • -xplatform 后面指定我们上面定义的架构信息文件夹。
#!/bin/sh
./configure \
-v \
-prefix /home/delvis/work/qt5.1.1 \
-release \
-opensource \
-no-accessibility \
-xplatform linux-arago-gnueabi-g++ \
-optimized-qmake \
-pch \
-qt-sql-sqlite \
-qt-zlib \
-no-opengl \
-no-sse2 \
-no-openssl \
-no-nis \
-no-cups \
-no-glib \
-no-pkg-config \
-nomake examples \
-lrt \
-no-separate-debug-info

执行sudo ./run.sh然后让它完成配置。

sudo make -j4 开始编译,编译的过程不可能是一帆风顺的,总会会出现各种的错误,自己找找错误解决吧

sudo make install之后进入我们-prefix的路径考培就好了。

拷贝文件到目标板并配置环境

1)传输Qt5.1.1到开发板

就是把我们刚才编译的环境拷贝到我们的目标板的目录下,可以使用FTP,可以使用OTG挂在U盘的方式,我喜欢使用SSH协议的scp命令。

sudo scp -r /home/delvis/work/qt5.1.1 [email protected]:/opt 输入密码之后就可以把整个文件夹传输到开发板上面了。

2) 配置开发板的环境

我们传输完毕之后,则需要在开发板上设定环境,当我们运行qt程序的时候才能寻找到这些Qt库文件。

打开

vi /etc/profile

增加以下,需要注意的是QT_ROOT,写对Qt的路径,还有TSLIB_ROOT编译的是tslib的路径。

export TSLIB_ROOT=/opt/tslib1.4                                                 
export QT_ROOT=/opt/qt5.1.1                                                
export TSLIB_TSDEVICE=/dev/input/event2                                         
export TSLIB_TSEVENTTYPE=input                                                  
export TSLIB_CONFFILE=/opt/tslib1.4/etc/ts.conf                                 
export TSLIB_PLUGINDIR=/opt/tslib1.4/lib/ts                                     
export TSLIB_CONSOLEDEVICE=none                                                 
export TSLIB_FBDEVICE=/dev/fb0                                                  
export QWS_MOUSE_PROTO=tslib:/dev/input/event2                                  
export LD_LIBRARY_PATH=/lib:/usr/lib:/usr/local/lib:$QT_ROOT/lib:$TSLIB_ROOT/lib
export QT_QPA_PLATFORM_PLUGIN_PATH=$QT_ROOT/plugins
export QT_QPA_PLATFORM=linuxfb:tty=/dev/fb0
export QT_QPA_FONTDIR=$QT_ROOT/lib/fonts
export QT_QPA_GENERIC_PLUGINS=tslib 

完成之后,更新一下环境。

source /etc/profile

完成配置。

3)测试

我们利用Qt Creator制作一个简单的界面,然后编译出可执行文件,讲可执行文件传输到目标板子上面执行,我们的可执行程序叫做TestEM,需要qws参数。

./TestEM -qws

然后就可以看见界面了。

参考文献:

[0] Carlos Wei著,Linux编译Qt4的环境_OMAPL138. CNBLOGS

[1] leocloud著 QT5.7交叉编译安装到arm. CSDN博客.

[2] 灿哥哥著. Qt5.7.0配置选项(configure options). CSDN博客.

[3] BigSam78. ARM 指令集版本和ARM 版本. 新浪博客.


版权声明:

1. 本文为MULTIBEANS团队研发跟随文章,未经允许不得转载。

2· 文中涉及的内容若有侵权行为,请与本人联系,本人会及时删除。

3· 尊重成果,本文将用的参考文献全部给出,向无私的工程师,爱好者致敬。

基于OMAPL138的字符驱动_GPIO驱动AD9833(三)之中断申请IRQ

基于OMAPL138的字符驱动_GPIO驱动AD9833(三)之中断申请IRQ

0. 导语

学习进入到了下一个阶段,还是以AD9833为例,这次学习是向设备申请中断,实现触发,在未来很多场景,比如做用户级的SPI传输完毕数据之后,怎么知道从设备要发数据呢,则需要一个IO信号通知主设备来读从设备的数据,那么就需要一个外部的IO中断信号,所以呢,对于中断的处理十分重要,本demo实现这样的一个功能**增加一个GPIO口,这个GPIO口接的是一个按键,通过触发信号,进入中断服务函数,在中断服务函数内改变AD9833的波形。**以此达到学习实验目的。

之前的代码都是一样的,在这里尽量剥离AD9833驱动和Linux的代码模板,只写中断相关相关程序。

效果演示视频: https://v.youku.com/v_show/id_XMzY4MjAwOTA0MA==.html?spm=a2h3j.8428770.3416059.1

1. 前篇导读:

  1. 基于OMAPL138的字符驱动_GPIO驱动AD9833(一)之ioctl

  2. 基于OMAPL138的Linux字符驱动_GPIO驱动AD9833(二)之cdev与read、write

  3. Linux GPIO键盘驱动开发记录_OMAPL138

原理图:

2. 申请中断准备

  • 首先需要两个头文件:

  • #include <linux/interrupt.h>

  • #include <linux/irq.h>

  • IO口配置准备
    在此次使用中断中,主要用的是GPIO口,我们使用电平跳变使之进入到中断处理程序中,所以作为IO口,需要配置IO口的方向为输入方向。我的OMAPL138中给的IO口操作使用GPIO_TO_PIN这个宏函数进行,在IO口操作上每个平台都会给定自己的寻IO口的方法,然后使用linux通用gpio_direction_output进行设定该GPIO口为输入方向,如上面的原理图,本demo的驱动使用的GPIO6[1],所以as follow:gpio_direction_output( GPIO_TO_PIN(6, 1) , 0 );

  • 硬件中断号IRQ
    我参考了很多文献,也找了很多书籍,对于硬件中断号码从哪里得到一笔带过,也没有具体说明,不过,经过一下午的努力,我找到了查找中断号码的方法。使用gpio_to_irq这个函数方法可以得到irq。我之前找到手册,看到了手册里面说GPIO6 BANK的IRQ为48号,我尝试加载到内核里面,每次初始化的时候都告诉我中断申请失败,这个号看来不是datasheet给定的48号,在Linux内核里面对于硬件IRQ又进行了重新映射。

  • 中断事件
    在内核中断申请的时候,需要指定中断事件是什么,边沿信号,高电平触发,低电平触发,在irq.h里面定义了:

	IRQ_TYPE_NONE		= 0x00000000,
	IRQ_TYPE_EDGE_RISING	= 0x00000001,
	IRQ_TYPE_EDGE_FALLING	= 0x00000002,
	IRQ_TYPE_EDGE_BOTH	= (IRQ_TYPE_EDGE_FALLING | IRQ_TYPE_EDGE_RISING),
	IRQ_TYPE_LEVEL_HIGH	= 0x00000004,
	IRQ_TYPE_LEVEL_LOW	= 0x00000008,
	IRQ_TYPE_LEVEL_MASK	= (IRQ_TYPE_LEVEL_LOW | IRQ_TYPE_LEVEL_HIGH),
	IRQ_TYPE_SENSE_MASK	= 0x0000000f	

我们需要指定这个事件。

  • 中断的名字
    这个使用#cat /proc/interrupts 查看当前内核中断资源的时候就可以看到你指定注册的名字。

  • 中断服务程序
    这个我们自己定一个函数就可以,然后一会儿使用中断申请的时候将数据传输进去就好。我们在中断服务函数里面进行按键进行波形切换:
static int key_count = 0;
static irqreturn_t	ad9833_press_intHandle( int irq, void *dev_id )
{
	printk( DRV_NAME "\t press trigger!\n" );
	if( key_count == 0 )  {
	    ad9833->set_wave_type( ad9833, SIN );
	    printk( DRV_NAME "\tSet wave is SIN.\n" );
	}else if( key_count == 2 ) {
	    ad9833->set_wave_type( ad9833, TRI );
	    printk( DRV_NAME "\tSet wave is TRI.\n" );
	}else if( key_count == 4 ) {
	    ad9833->set_wave_type( ad9833, SQU );
	    printk( DRV_NAME "\tSet wave is SQU.\n" );
	}
	key_count ++;
	if( key_count >= 5 )
	    key_count = 0;

	return	IRQ_RETVAL( IRQ_HANDLED );
}

3. 申请中断准备

使用request_irq函数就好,我们在初始化函数里面,申请irq。在申请irq前,为了更好的管理中断函数,我们定义了一个结构体,专门进行irq配置。

struct gpio_irq_desc {

	int irq;
	unsigned long flags;
	char *name;

} press_dev_desc = {

		0,
		IRQ_TYPE_EDGE_FALLING,
		"sw6_push_button"

};

第一个是irq,我们在向内核申请中断的时候会使用gpio_to_irq进行irq的赋值,flags就是中断事件的触发条件,这里是下降边沿触发,最后一个name就是上面注册号中断分配的名字,初始化程序如下:

	/*
	 * interrupt apply
	 * */
	press_dev_desc.irq =  gpio_to_irq(ad9833_gpios[3]);
	ret =	request_irq( press_dev_desc.irq , &ad9833_press_intHandle, press_dev_desc.flags, press_dev_desc.name, (void*)0 );
	if( ret ) {
		printk( DRV_NAME "\terror %d: IRQ = %d number failed!\n",ret,gpio_to_irq(ad9833_gpios[3]) );
		ret = -EBUSY;
	    unregister_chrdev_region( devno,1 );
		for( i = 0; i < ARRAY_SIZE(ad9833_gpios); i++)
		    gpio_free( ad9833_gpios[i] );
		kfree(ad9833);
        return ret;
	}
	printk( DRV_NAME "\tiqr apply ok!!\n" );
	

到此我们就完成了中断配置。

4. 中断释放

使用freqq_irq进行释放。这个函数应该放在exit驱动的函数里面。
free_irq( press_dev_desc.irq, (void*)0 );

源代码下载

链接: https://pan.baidu.com/s/1JgPgGP1Ag_oixHmHOy3QEw 密码: 5x84

参考文献

[1] 创龙电子科技, OMAPL138的GPIO输出输入
, 百度文库, 2014年5月8日

[2] wh_19910525, Linux的 标准GPIO及中断API函数
, CSDN博客, 2013年12月25日

[3] wangcong02345, Linux内核---44.关于中断号与中断引脚
, CSDN博客, 2016年7月9日

[4] GreenHand#, Linux设备驱动中断机制
, CNBLOGS, 2016年12月27日

Linux Driver - GPIO键盘驱动开发记录_OMAPL138

Linux GPIO键盘驱动开发记录_OMAPL138

Linux基本配置完毕了,这几天开始着手Linux驱动的开发,从一个最简单的键盘驱动开始,逐步的了解开发驱动的过程有哪些。看了一下Linux3.3内核文件下的driver目录,点开里面的C文件,感觉底层的Linux驱动机制还是很复杂的,还需要一段漫长时间的学习。现在开发的也不能说是叫做驱动,也只能说是驱动的应用,我们学习驱动也从应用逐步开始,往里面深入吧。

0.开发准备

  • 内核源文件(当时我们编译内核时候的目录,很重要,编译驱动的时候需要依赖这些内核源文件)
  • Makefile文件(编译驱动的Makefile文件)
  • 驱动源程序
  • 应用程序(有main函数的)

1.键盘的接线图

我们主要使用USER0和USER1 KEY,两个按键,完成Linux GPIO键盘驱动开发。从图中可以看出GPIO0_6和GPIO6_1主要采集键盘按下的信息。

2. key.c驱动文件

驱动结构可以看上图,主要是在注册Linux内核设备,我们使用platform_device进行内核注册。我们从思维到图上看,从后往前的顺序进行一个一个定义。

2.1 gpio_key_buttons结构体

**Linux提供的标准结构体,在#include <linux/gpio_key.h>头文件中。**这里面就要定义按键的行为信息和指定GPIO口,这个是和上面的硬件原理图打交道的一个结构体。以下是和这个结构体有关的定义。

#include <linux/gpio.h>
#include <linux/gpio_keys.h>
#include <mach/da8xx.h>						// 板子的头文件

#define             OMAPL138_KEYS_BEBOUNCE          10
#define             OMAPL138_GPIO_KEYS_POLL_MS      200

#define             OMAPL138_USER_KEY0              GPIO_TO_PIN( 0,6 )
#define             OMAPL138_USER_KEY1              GPIO_TO_PIN( 6,1 )

static const short  omapl138_user_key_pins[] = {
    DA850_GPIO0_6, DA850_GPIO6_1, -1
};

static struct   gpio_keys_button omapl138_user_keys[]   =   {
    [0] = {
        .type               =   EV_KEY,
        .active_low         =   1,
        .wakeup             =   0,
        .debounce_interval  =   OMAPL138_KEYS_BEBOUNCE,
        .code               =   KEY_PROG1,
        .desc               =   "user_key0",
        .gpio               =   OMAPL138_USER_KEY0
    },
    [1] = {
        .type               =   EV_KEY,
        .active_low         =   1,
        .wakeup             =   0,
        .debounce_interval  =   OMAPL138_KEYS_BEBOUNCE,
        .code               =   KEY_PROG2,
        .desc               =   "user_key1",
        .gpio               =   OMAPL138_USER_KEY1
    }
};

在其他的平台可能GPIO口的定义不同,我用的是OMAPL138,使用的内核文件是OMAPL138提供的,他的里面定义了da8xx.h定义了相关GPIO的宏定义,你需要找到你自己平台GPIO结构体的定义,修改这个宏定义即可。这个结构提十分具备可读性,一看就能看懂了,其中的.code就是我们写应用程序的时候,按键读出来的值,就可以判断了。

2.2 gpio_keys_platform_data结构体

platform_data结构体顾名思义,基本上就是和我们整个驱动开发数据相关的。

static struct   gpio_keys_platform_data     omapl138_user_keys_pdata    =   {
    .buttons                =   omapl138_user_keys,
    .nbuttons               =   ARRAY_SIZE( omapl138_user_keys )
};

里面的成员,.buttons就是刚才我们定义gpio_keys_button数组,.nbuttons就是长度,我们使用宏函数ARRAY_SIZE完成取值。

2.3 platform_device结构体

思维导图在向前,我们就需要定义platform_device结构体了,这个结构体就是要和Linux内核打交道的结构提了。里面有设备名称.name,.id,dev

static void     omapl138_user_keys_release( struct device *dev ) {

}
static struct   platform_device     omapl138_user_keys_device   =   {
        // you can find mount root by command "dmesg | grep gpio-keys ";
        // cat /proc/bus/input/devices  look the where the device is.
        .name               =   "gpio-keys",
        .id                 =   1,
        .dev                =   {
                            .platform_data  =   &omapl138_user_keys_pdata,
                            .release    =   omapl138_user_keys_release,
        },
};

.name中当驱动程序向linux注册后通过dmesg | grep gpio-keys命令就可以看到这个名字,在内核调试输出中可以看到。.id设备挂载节点的id,同样的设备id不能重复。.dev给定data和release函数(可以为空)。

2.4 初始化函数和退出函数

最后就是初始化函数和退出函数,最终要的就是resigster这个函数了。向内核注册设备。

static int __init omapl138_user_keys_init( void )
{
    int reg;
    reg     =   platform_device_register( &omapl138_user_keys_device );
    if( reg ) {
        pr_warning( "Could not register baseboard GPIO tronlong keys!" );
    }else {
        printk( KERN_INFO "User keys register successful!" );
    }
    return reg;
}

static void __exit omapl138_user_keys_exit( void )
{
    platform_device_unregister( &omapl138_user_keys_device );
    printk( KERN_INFO   "user keys unregister ! \n" );
}


module_init( omapl138_user_keys_init );
module_exit( omapl138_user_keys_exit );

MODULE_DESCRIPTION( "user keys platform driver," );
MODULE_AUTHOR("Carlos Wei");
MODULE_LICENSE( "GPL" );

当我们使用insmod name.ko的时候,自动调用module_init()里面写入的函数了,当我们rmmod name.ko的时候,自动调用module_exit()里面写入的函数。

3 编译内核或者以模块形式加载

我们可以把这个驱动程序静态编译到内核代码树中也可以以模块的形式加载到内核中,我建议一开始开发的时候使用模块的形式加入到内核中,等着编译成熟之后,在编译到内核里面。

3.1 编译

制定内核源码文件的路径,写好Makefile文件,编译后上传到目标板的任意路径。

3.1.1 Makefile文件

ifneq ($(KERNELRELEASE),)

obj-m := key.o

else

all:
	make -C $(KDIR) M=$(PWD) modules ARCH=arm CROSS_COMPILE=arm-arago-linux-gnueabi-

clean:
	rm -rf *.ko *.o *.mod.o *.mod.c *.symvers  modul* .button.* .tmp_versions

KDIR=/usr/src/linux-headers-3.3.0-omapl138

endif

这里比较重要的:

  • KDIR=? 这个位置就是你Linux源码文件的路径,(内核事先编译一定要正确
  • CROSS_COMPILE= 这个指定交叉编译器,我的交叉编译器名字比较怪异arm-arago-linux-gnueabi-

3.1.2 Make一下生成key.ko

编译完成之后,通过ls命令,查看生成了key.ko文件。

通过scp或者ftp把key.ko文件传输到目标板子上。

3.2 加载驱动与查看挂载节点

1) insmod key.ko

2) 通过dmesg查看内核输出是否成功

3)通过cat /proc/bus/input/devices命令查看挂在详情

重点是event1,一会儿我们编辑应用程序需要打开/dev/input/event1这个设备节点进行对GPIO键盘操作。

4)测试键盘输入

在没有编写应用程序的时候,我们也可以通过简单的方法对GPIO键盘进行一个简单的测试。GPIO挂载节点是Handlers = event1 则输入:

cat /dev/input/event1

然后我们按键盘,如果当我们点击键盘的时候 终端输出乱码则代表我们的驱动是编写成功的。

5) 解挂驱动

如果我们不需要驱动了,则需要进行解挂驱动:

rmmod key.ko

也可以通过dmesg命令查看内核输出日志。

4 应用程序开发

建立文件:key_app.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <time.h>
#include <fcntl.h>
#include <linux/input.h>

int main( int argc, char **argv  )
{
    int key_state;
    int fd;
    int ret;
    int code;
    struct input_event buf;

    fd = open( "/dev/input/event1", O_RDONLY );
    if( fd  < 0 ) {
        printf( "Open GPIO_keys failed!!\n" );
        return -1;
    }

    printf( "Open GPIO keys successful! \n" );
    while( 1 ) {
        ret = read( fd, &buf, sizeof( struct input_event ) );
        if( ret <= 0 ) {
            printf( "read failed! \n" );
            return -1;
        }
        code = buf.code;
        key_state   =   buf.value;
        printf("wait...... \n");
        switch( code ) {
            case KEY_PROG1:

                code = '1';
                printf( "KEY1 state = %d\n", key_state );
                break;
            case KEY_PROG2:

                code = '2';
                printf( "KEY2 state = %d\n", key_state );
                break;
        }


    }
    printf("key test finished.\n");
    close(fd);
    return 0;
}

一个非常简单的程序,就是输出案件之。我们编译它:

arm-arago-linux-gnueabi-gcc key_app.c -o key_app.o

生成的key_app.o文件放入目标板的Linux目录,然后运行即可。

5 调试结果

通过按键就可以输出这些值,意味着我们的驱动开发成功了。

附录:源程序

链接: https://pan.baidu.com/s/1pNcEBEj 密码: xpsq


03_ELF文件_静态链接

03_ELF文件_静态链接

1. Pre-condition

前面两节是对单独的c文件编译出的elf文件进行解析,静态链接是对多个c文件编译出的二进制文件进行合并的过程。我们对下面的简单的c文件进行静态链接。本文基于aarch64 armv8体系架构编译出的文件对多个目标文件的链接过程做出探究。

flowchart LR
    A[a.c]-->B[a.o]-->C[ab.o]-->D[ab.elf]
    E[b.c]-->F[b.o]-->C
    G[libc]-->D
Loading

1.1 c文件

File1: a.c

extern int b_share;
extern int b_func(int c, int d);

static int a_0 = 0x7f;
static const char *a_1 = "aaaaaaaaaaaa";
static int a_3;
static int a_4 = 0;

static int a_func(int a, int b)
{
    return a - b;
}

int main(void)
{
    int m = 0;
    a_0 ++;
    a_1[5];
    a_3 = 1;
    a_4 = 5;
    m = b_func(m, b_share);
    m = a_func(m, b_share);
    return 0;
}

File2: b.c

int b_share = 0xFF;

static int b_0 = 0x7f;
static const char *b_1 = "hello world";
static int b_3;
static int b_4 = 0;

int b_func(int c, int d)
{
    static int b_5 = 0xAA;
    int a = 0;
    b_0 ++;
    b_1[5];
    b_3 = 1;
    b_4 = 5;
    a = c + d;
    return a;
}

1.2 o及asm文件

编译: aarch64-linux-gnu-gcc -c a.c b.c 分别编译出a.o, b.o

我们查看一下a.o和b.o的具体的指令段

aarch64-linux-gnu-objdump -s -d a.o

a.o:     file format elf64-littleaarch64

Contents of section .text:
 0000 ff4300d1 e00f00b9 e10b00b9 e10f40b9  .C............@.
 0010 e00b40b9 2000004b ff430091 c0035fd6  ..@. ..K.C...._.
 0020 fd7bbea9 fd030091 bf1f00b9 00000090  .{..............
 0030 00000091 000040b9 01040011 00000090  ......@.........
 0040 00000091 010000b9 00000090 00000091  ................
 0050 21008052 010000b9 00000090 00000091  !..R............
 0060 a1008052 010000b9 00000090 00000091  ...R............
 0070 000040b9 e103002a a01f40b9 00000094  ..@....*..@.....
 0080 a01f00b9 00000090 00000091 000040b9  ..............@.
 0090 e103002a a01f40b9 daffff97 a01f00b9  ...*..@.........
 00a0 00008052 fd7bc2a8 c0035fd6           ...R.{...._.    
Contents of section .data:
 0000 7f000000 00000000 00000000 00000000  ................
Contents of section .rodata:
 0000 61616161 61616161 61616161 00        aaaaaaaaaaaa.   
Contents of section .comment:
 0000 00474343 3a20284c 696e6172 6f204743  .GCC: (Linaro GC
 0010 4320372e 352d3230 31392e31 32292037  C 7.5-2019.12) 7
 0020 2e352e30 00                          .5.0.           

Disassembly of section .text:

0000000000000000 <a_func>:
   0:   d10043ff        sub     sp, sp, #0x10
   4:   b9000fe0        str     w0, [sp, #12]
   8:   b9000be1        str     w1, [sp, #8]
   c:   b9400fe1        ldr     w1, [sp, #12]
  10:   b9400be0        ldr     w0, [sp, #8]
  14:   4b000020        sub     w0, w1, w0
  18:   910043ff        add     sp, sp, #0x10
  1c:   d65f03c0        ret

0000000000000020 <main>:
  20:   a9be7bfd        stp     x29, x30, [sp, #-32]!
  24:   910003fd        mov     x29, sp
  28:   b9001fbf        str     wzr, [x29, #28]
  2c:   90000000        adrp    x0, 0 <a_func>
  30:   91000000        add     x0, x0, #0x0
  34:   b9400000        ldr     w0, [x0]
  38:   11000401        add     w1, w0, #0x1
  3c:   90000000        adrp    x0, 0 <a_func>
  40:   91000000        add     x0, x0, #0x0
  44:   b9000001        str     w1, [x0]
  48:   90000000        adrp    x0, 0 <a_func>
  4c:   91000000        add     x0, x0, #0x0
  50:   52800021        mov     w1, #0x1                        // #1
  54:   b9000001        str     w1, [x0]
  58:   90000000        adrp    x0, 0 <a_func>
  5c:   91000000        add     x0, x0, #0x0
  60:   528000a1        mov     w1, #0x5                        // #5
  64:   b9000001        str     w1, [x0]
  68:   90000000        adrp    x0, 0 <b_share>
  6c:   91000000        add     x0, x0, #0x0
  70:   b9400000        ldr     w0, [x0]
  74:   2a0003e1        mov     w1, w0
  78:   b9401fa0        ldr     w0, [x29, #28]
  7c:   94000000        bl      0 <b_func>
  80:   b9001fa0        str     w0, [x29, #28]
  84:   90000000        adrp    x0, 0 <b_share>
  88:   91000000        add     x0, x0, #0x0
  8c:   b9400000        ldr     w0, [x0]
  90:   2a0003e1        mov     w1, w0
  94:   b9401fa0        ldr     w0, [x29, #28]
  98:   97ffffda        bl      0 <a_func>
  9c:   b9001fa0        str     w0, [x29, #28]
  a0:   52800000        mov     w0, #0x0                        // #0
  a4:   a8c27bfd        ldp     x29, x30, [sp], #32
  a8:   d65f03c0        ret

aarch64-linux-gnu-objdump -s -d b.o

$ aarch64-linux-gnu-objdump -s -d b.o

b.o:     file format elf64-littleaarch64

Contents of section .text:
 0000 ff8300d1 e00f00b9 e10b00b9 ff1f00b9  ................
 0010 e10f40b9 e00b40b9 2000000b e01f00b9  ..@...@. .......
 0020 e01f40b9 ff830091 c0035fd6           ..@......._.    
Contents of section .data:
 0000 ff000000                             ....            
Contents of section .comment:
 0000 00474343 3a202847 4e552054 6f6f6c63  .GCC: (GNU Toolc
 0010 6861696e 20666f72 20746865 2041726d  hain for the Arm
 0020 20417263 68697465 63747572 65203131   Architecture 11
 0030 2e322d32 3032322e 30322028 61726d2d  .2-2022.02 (arm-
 0040 31312e31 34292920 31312e32 2e312032  11.14)) 11.2.1 2
 0050 30323230 31313100                    0220111.        

Disassembly of section .text:

0000000000000000 <b_fb.o:     file format elf64-littleaarch64

Contents of section .text:
 0000 ff8300d1 e00f00b9 e10b00b9 ff1f00b9  ................
 0010 00000090 00000091 000040b9 01040011  ..........@.....
 0020 00000090 00000091 010000b9 00000090  ................
 0030 00000091 21008052 010000b9 00000090  ....!..R........
 0040 00000091 a1008052 010000b9 e10f40b9  .......R......@.
 0050 e00b40b9 2000000b e01f00b9 e01f40b9  ..@. .........@.
 0060 ff830091 c0035fd6                    ......_.        
Contents of section .data:
 // a.o
 0000 ff000000 7f000000 00000000 00000000  ................
 // b.p
 0010 aa000000                             ....            
Contents of section .rodata:
 0000 68656c6c 6f20776f 726c6400           hello world.    
Contents of section .comment:
 0000 00474343 3a20284c 696e6172 6f204743  .GCC: (Linaro GC
 0010 4320372e 352d3230 31392e31 32292037  C 7.5-2019.12) 7
 0020 2e352e30 00                          .5.0.           

Disassembly of section .text:

0000000000000000 <b_func>:
   0:   d10083ff        sub     sp, sp, #0x20
   4:   b9000fe0        str     w0, [sp, #12]
   8:   b9000be1        str     w1, [sp, #8]
   c:   b9001fff        str     wzr, [sp, #28]
  10:   90000000        adrp    x0, 0 <b_func>
  14:   91000000        add     x0, x0, #0x0
  18:   b9400000        ldr     w0, [x0]
  1c:   11000401        add     w1, w0, #0x1
  20:   90000000        adrp    x0, 0 <b_func>
  24:   91000000        add     x0, x0, #0x0
  28:   b9000001        str     w1, [x0]
  2c:   90000000        adrp    x0, 0 <b_func>
  30:   91000000        add     x0, x0, #0x0
  34:   52800021        mov     w1, #0x1                        // #1
  38:   b9000001        str     w1, [x0]
  3c:   90000000        adrp    x0, 0 <b_func>
  40:   91000000        add     x0, x0, #0x0
  44:   528000a1        mov     w1, #0x5                        // #5
  48:   b9000001        str     w1, [x0]
  4c:   b9400fe1        ldr     w1, [sp, #12]
  50:   b9400be0        ldr     w0, [sp, #8]
  54:   0b000020        add     w0, w1, w0
  58:   b9001fe0        str     w0, [sp, #28]
  5c:   b9401fe0        ldr     w0, [sp, #28]
  60:   910083ff        add     sp, sp, #0x20
  64:   d65f03c0        retunc>:
   0:   d10083ff        sub     sp, sp, #0x20
   4:   b9000fe0        str     w0, [sp, #12]
   8:   b9000be1        str     w1, [sp, #8]
   c:   b9001fff        str     wzr, [sp, #28]
  10:   b9400fe1        ldr     w1, [sp, #12]
  14:   b9400be0        ldr     w0, [sp, #8]
  18:   0b000020        add     w0, w1, w0
  1c:   b9001fe0        str     w0, [sp, #28]
  20:   b9401fe0        ldr     w0, [sp, #28]
  24:   910083ff        add     sp, sp, #0x20
  28:   d65f03c0        ret

1.3 ab.o合体文件

使用aarch64-linux-gnu-gcc -c a.o b.o -o ab.o 生成ab.o合体文件。

ab.o:     file format elf64-littleaarch64

Contents of section .text:
 // a.o
 4000e8 ff4300d1 e00f00b9 e10b00b9 e10f40b9  .C............@.
 4000f8 e00b40b9 2000004b ff430091 c0035fd6  ..@. ..K.C...._.
 400108 fd7bbea9 fd030091 bf1f00b9 80000090  .{..............
 400118 00800891 000040b9 01040011 80000090  ......@.........
 400128 00800891 010000b9 80000090 00100991  ................
 400138 21008052 010000b9 80000090 00200991  !..R......... ..
 400148 a1008052 010000b9 80000090 00c00891  ...R............
 400158 000040b9 e103002a a01f40b9 0c000094  ..@....*..@.....
 400168 a01f00b9 80000090 00c00891 000040b9  ..............@.
 400178 e103002a a01f40b9 daffff97 a01f00b9  ...*..@.........
 // b.o(从ff8300d1开始)
 400188 00008052 fd7bc2a8 c0035fd6 ff8300d1  ...R.{...._.....
 400198 e00f00b9 e10b00b9 ff1f00b9 80000090  ................
 4001a8 00d00891 000040b9 01040011 80000090  ......@.........
 4001b8 00d00891 010000b9 80000090 00300991  .............0..
 4001c8 21008052 010000b9 80000090 00400991  !..R.........@..
 4001d8 a1008052 010000b9 e10f40b9 e00b40b9  ...R......@...@.
 4001e8 2000000b e01f00b9 e01f40b9 ff830091   .........@.....
 4001f8 c0035fd6                             .._.            
Contents of section .rodata:
 // a.o
 400200 61616161 61616161 61616161 00000000  aaaaaaaaaaaa....
 // b.o
 400210 68656c6c 6f20776f 726c6400           hello world.    
Contents of section .data:
 410220 7f000000 00000000 00024000 00000000  ..........@.....
 410230 ff000000 7f000000 10024000 00000000  ..........@.....
 410240 aa000000                             ....            
Contents of section .comment:
 0000 4743433a 20284c69 6e61726f 20474343  GCC: (Linaro GCC
 0010 20372e35 2d323031 392e3132 2920372e   7.5-2019.12) 7.
 0020 352e3000                             5.0.            

Disassembly of section .text:

00000000004000e8 <a_func>:
  4000e8:       d10043ff        sub     sp, sp, #0x10
  4000ec:       b9000fe0        str     w0, [sp, #12]
  4000f0:       b9000be1        str     w1, [sp, #8]
  4000f4:       b9400fe1        ldr     w1, [sp, #12]
  4000f8:       b9400be0        ldr     w0, [sp, #8]
  4000fc:       4b000020        sub     w0, w1, w0
  400100:       910043ff        add     sp, sp, #0x10
  400104:       d65f03c0        ret

0000000000400108 <main>:
  400108:       a9be7bfd        stp     x29, x30, [sp, #-32]!
  40010c:       910003fd        mov     x29, sp
  400110:       b9001fbf        str     wzr, [x29, #28]
  400114:       90000080        adrp    x0, 410000 <b_func+0xfe6c>
  400118:       91088000        add     x0, x0, #0x220
  40011c:       b9400000        ldr     w0, [x0]
  400120:       11000401        add     w1, w0, #0x1
  400124:       90000080        adrp    x0, 410000 <b_func+0xfe6c>
  400128:       91088000        add     x0, x0, #0x220
  40012c:       b9000001        str     w1, [x0]
  400130:       90000080        adrp    x0, 410000 <b_func+0xfe6c>
  400134:       91091000        add     x0, x0, #0x244
  400138:       52800021        mov     w1, #0x1                        // #1
  40013c:       b9000001        str     w1, [x0]
  400140:       90000080        adrp    x0, 410000 <b_func+0xfe6c>
  400144:       91092000        add     x0, x0, #0x248
  400148:       528000a1        mov     w1, #0x5                        // #5
  40014c:       b9000001        str     w1, [x0]
  400150:       90000080        adrp    x0, 410000 <b_func+0xfe6c>
  400154:       9108c000        add     x0, x0, #0x230
  400158:       b9400000        ldr     w0, [x0]
  40015c:       2a0003e1        mov     w1, w0
  400160:       b9401fa0        ldr     w0, [x29, #28]
  400164:       9400000c        bl      400194 <b_func>
  400168:       b9001fa0        str     w0, [x29, #28]
  40016c:       90000080        adrp    x0, 410000 <b_func+0xfe6c>
  400170:       9108c000        add     x0, x0, #0x230
  400174:       b9400000        ldr     w0, [x0]
  400178:       2a0003e1        mov     w1, w0
  40017c:       b9401fa0        ldr     w0, [x29, #28]
  400180:       97ffffda        bl      4000e8 <a_func>
  400184:       b9001fa0        str     w0, [x29, #28]
  400188:       52800000        mov     w0, #0x0                        // #0
  40018c:       a8c27bfd        ldp     x29, x30, [sp], #32
  400190:       d65f03c0        ret

0000000000400194 <b_func>:
  400194:       d10083ff        sub     sp, sp, #0x20
  400198:       b9000fe0        str     w0, [sp, #12]
  40019c:       b9000be1        str     w1, [sp, #8]
  4001a0:       b9001fff        str     wzr, [sp, #28]
  4001a4:       90000080        adrp    x0, 410000 <b_func+0xfe6c>
  4001a8:       9108d000        add     x0, x0, #0x234
  4001ac:       b9400000        ldr     w0, [x0]
  4001b0:       11000401        add     w1, w0, #0x1
  4001b4:       90000080        adrp    x0, 410000 <b_func+0xfe6c>
  4001b8:       9108d000        add     x0, x0, #0x234
  4001bc:       b9000001        str     w1, [x0]
  4001c0:       90000080        adrp    x0, 410000 <b_func+0xfe6c>
  4001c4:       91093000        add     x0, x0, #0x24c
  4001c8:       52800021        mov     w1, #0x1                        // #1
  4001cc:       b9000001        str     w1, [x0]
  4001d0:       90000080        adrp    x0, 410000 <b_func+0xfe6c>
  4001d4:       91094000        add     x0, x0, #0x250
  4001d8:       528000a1        mov     w1, #0x5                        // #5
  4001dc:       b9000001        str     w1, [x0]
  4001e0:       b9400fe1        ldr     w1, [sp, #12]
  4001e4:       b9400be0        ldr     w0, [sp, #8]
  4001e8:       0b000020        add     w0, w1, w0
  4001ec:       b9001fe0        str     w0, [sp, #28]
  4001f0:       b9401fe0        ldr     w0, [sp, #28]
  4001f4:       910083ff        add     sp, sp, #0x20
  4001f8:       d65f03c0        ret

2. 链接器对elf的空间与地址分配

2.1 相似段合并

链接器的作用就是对几个目标文件合并成一个输出文件,从上面的例子中可以看出,a.o和b.o合并为ab.o,ab.o和clib里面的函数合并称为ab.elf文件。a.o和b.o文件合并为ab.o采用的是相似段合并的策略,把相似的段合并到一起,在合成ab.o文件之后,.text段连续的将a.o和b.o的.text段合并到了一起,.data段也类似。这里需要注意的是:

  • .bss段:bss段在目标文件不占用文件的空间(输出可执行文件的空间),但是bss段在装载时占用地址空间(虚拟地址空间分配)
  • 这里有个虚拟地址空间分配的概念,对于.text和.data可执行文件和虚拟地址空间都需要有空间分配。

2.2 两步链接(Two-pass Linking)

  • 空间与地址分配
  • 符号解析与重定位

探究一下合并之后段发生变化的过程:

aarch64-linux-gnu-objdump -h a.o

aarch64-linux-gnu-objdump -h b.o

aarch64-linux-gnu-ld a.o b.o -e main -o ab

aarch64-linux-gnu-objdump -h ab

a.o:     file format elf64-littleaarch64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         000000ac  0000000000000000  0000000000000000  00000040  2**2
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000010  0000000000000000  0000000000000000  000000f0  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, DATA
  2 .bss          00000008  0000000000000000  0000000000000000  00000100  2**2
                  ALLOC
  3 .rodata       0000000d  0000000000000000  0000000000000000  00000100  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .comment      00000025  0000000000000000  0000000000000000  0000010d  2**0
                  CONTENTS, READONLY
  5 .note.GNU-stack 00000000  0000000000000000  0000000000000000  00000132  2**0
                  CONTENTS, READONLY
      
b.o:     file format elf64-littleaarch64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000068  0000000000000000  0000000000000000  00000040  2**2
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000014  0000000000000000  0000000000000000  000000a8  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, DATA
  2 .bss          00000008  0000000000000000  0000000000000000  000000bc  2**2
                  ALLOC
  3 .rodata       0000000c  0000000000000000  0000000000000000  000000c0  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .comment      00000025  0000000000000000  0000000000000000  000000cc  2**0
                  CONTENTS, READONLY
  5 .note.GNU-stack 00000000  0000000000000000  0000000000000000  000000f1  2**0
                  CONTENTS, READONLY
                  
ab:     file format elf64-littleaarch64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000114  00000000004000e8  00000000004000e8  000000e8  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .rodata       0000001c  0000000000400200  0000000000400200  00000200  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  2 .data         00000024  0000000000410220  0000000000410220  00000220  2**3
                  CONTENTS, ALLOC, LOAD, DATA
  3 .bss          00000014  0000000000410244  0000000000410244  00000244  2**2
                  ALLOC
  4 .comment      00000024  0000000000000000  0000000000000000  00000244  2**0
                  CONTENTS, READONLY

关于VMA和LMA的概念这里需要阐述一下1

Every loadable or allocatable output section has two addresses. The first is the VMA, or virtual memory address. This is the address the section will have when the output file is run. The second is the LMA, or load memory address. This is the address at which the section will be loaded. In most cases the two addresses will be the same. An example of when they might be different is when a data section is loaded into ROM, and then copied into RAM when the program starts up (this technique is often used to initialize global variables in a ROM based system). In this case the ROM address would be the LMA, and the RAM address would be the VMA.

总结下来:

  • VMA和LMA一般情况下都是一致的
  • 嵌入式系统里面,程序放在ROM中的,LMA通常为加载后的RAM的地址,VMA通常为ROM的地址。
  • 在ARMv8体系架构里面,VMA和LMA是一致的。

06_ARMv8_指令集_一些重要的指令 的1.4部分,可以看到嵌入式系统里面LMA和VMA不一样的例子,ARMv8的LDR指令和ADRP指令加载的地址上对VMA和LMA由非常明显的区别。

image-20220323103217350

  • 注意1,如果对于单个的.o文件,LMA和VMA地址全都是0,因为虚拟内存空间还没有被分配,等到链接之后,VM和LM都已经有值了,
  • 注意2,.data段在操作系统中是从0x00410220开始分配的,.text段是在操作系统中0x004000e8分配的。
  • 注意3,bss的section最后叠加出来的,并非a和b之和,而多了4个字节。在a.o和b.o文件中,bss段还没有展开,只是有个占位符,等着进行链接之后,才会给分配地址,才有了真正意义的.bss。所以说多了4个字节并不准确,是在链接之后才开始对bss处理。

对于注意2,在《程序员的自我修养》一书中p103提到:

在Linux下,ELF可执行文件默认从地址0x08048000开始分配。

对于这个设定,在网上也能找到相关资料2,但是在x86的体系架构中和arm体系架构中对文件进行编译,得到的.data段却有着不同的结果,是从0x00400000开始分配的。这个似乎和书上讲的不一致。本文先留个悬念,后面到虚拟内存映射再回来解答这个问题。

2.3 符号解析与重定位

a.c文件中引用了b_func,如果单独gcc -c a.c文件,此时b_func并没有值,因此在汇编上可以看到指令 ,此时跳转指令bl 地址为0。

  7c:   94000000        bl      0 <b_func>

然而在ab文件汇编里面,已经进行了符号重定位,则在ab文件的汇编中可以看到:

  400164:       9400000c        bl      400194 <b_func>

看到bl指令后面已经有地址 0x400194,正是b_func的入口程序。前后比较main的变化,左边是未链接的main函数,右边是经过链接的main函数

image-20220323114805949

上面需要调整的地方有个重定位表(Relocation Table)保存这些重定位的信息:

aarch-linux-gnu-objdump -r a.o

a.o:     file format elf64-littleaarch64

RELOCATION RECORDS FOR [.text]:
OFFSET           TYPE              VALUE 
000000000000002c R_AARCH64_ADR_PREL_PG_HI21  .data
0000000000000030 R_AARCH64_ADD_ABS_LO12_NC  .data
000000000000003c R_AARCH64_ADR_PREL_PG_HI21  .data
0000000000000040 R_AARCH64_ADD_ABS_LO12_NC  .data
0000000000000048 R_AARCH64_ADR_PREL_PG_HI21  .bss
000000000000004c R_AARCH64_ADD_ABS_LO12_NC  .bss
0000000000000058 R_AARCH64_ADR_PREL_PG_HI21  .bss+0x0000000000000004
000000000000005c R_AARCH64_ADD_ABS_LO12_NC  .bss+0x0000000000000004
0000000000000068 R_AARCH64_ADR_PREL_PG_HI21  b_share
000000000000006c R_AARCH64_ADD_ABS_LO12_NC  b_share
000000000000007c R_AARCH64_CALL26  b_func
0000000000000084 R_AARCH64_ADR_PREL_PG_HI21  b_share
0000000000000088 R_AARCH64_ADD_ABS_LO12_NC  b_share


RELOCATION RECORDS FOR [.data]:
OFFSET           TYPE              VALUE 
0000000000000008 R_AARCH64_ABS64   .rodata

可以看到OFFSET就是上面compare结果需要修改的位置。

使用readelf -s a.o可以读出哪里的符号没有被定义:

Symbol table '.symtab' contains 20 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS a.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 
     5: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT    3 $d
     6: 0000000000000000     4 OBJECT  LOCAL  DEFAULT    3 a_0
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 
     8: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT    6 $d
     9: 0000000000000008     8 OBJECT  LOCAL  DEFAULT    3 a_1
    10: 0000000000000000     4 OBJECT  LOCAL  DEFAULT    5 a_3
    11: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT    5 $d
    12: 0000000000000004     4 OBJECT  LOCAL  DEFAULT    5 a_4
    13: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT    1 $x
    14: 0000000000000000    32 FUNC    LOCAL  DEFAULT    1 a_func
    15: 0000000000000000     0 SECTION LOCAL  DEFAULT    8 
    16: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 
    17: 0000000000000020   140 FUNC    GLOBAL DEFAULT    1 main
    18: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND b_share
    19: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND b_func

第18/19行,可以看到b_share和b_func没有被定义。

2.4 Common block

如果有全局未初始化的变量,该符号会被认为是弱符号,同样的通过readelf -s也可以看到该符号被标记为COM。

对于c.c文件:

int c_share = 0xFF;

static int c_0 = 0x7f;
static const char *c_1 = "ccccccccccc";
static int c_3;
static int c_4 = 0;

int b_func(int c, int d)
{
    static int c_5 = 0xAA;
    int a = 0;
    c_0 ++;
    c_1[5];
    c_3 = 1;
    c_4 = 5;
    a = c_3 + c_4 + c_0;
    return a;
}

image-20220323122937142

c_3被标记为COM,且占4个字节。这里就存在一种冲突的情况。

  • 若在其中一个c文件中,定义了int c_3,未初始化,又在另一个文件里面定义了 double c_3,未初始化。按照common的链接规则,以最大的size为准,因此这里就是使用double c_3的size,为8。如果定义了强符号,谁强,则按照谁的来。

  • 如果弱符号的长度大于强符号,ld链接器会报错:

    ld warning: alignment 4 of symbol xxx is smaller than 8

  • 如果在多个文件中没有extern关键字,是的编译器在多个目标文件中产生同一个变量的定义。编译器通常把未初始化的变量当做COMMON类型处理。GCC提供方法让我们把所有的未初始化的全局变量不以COMMON类型处理:

    • 使用编译选项: -fno-common
    • 使用attribute扩展: int c_3 __attribute__((nocommon))
  • 如果不以common形式处理,那么在链接的时候遇到相同的符号,就会发生编译报错。

3. 静态库链接

3.1 静态链接库文件生成

  • 准备d.c文件,里面提供sum, sub 和 abs函数,其中sum和sub有本身提供,abs依赖于另一个库文件中的函数,编译成d.a文件。
  • 准备e.c文件,里面提供e_ab函数供abs调用,编译成e.a文件。
  • 准备f.c文件,为main函数调用,使用这个库。
flowchart LR
    A[d.c]-->|gcc -c|B[d.o]-->|ar -crv|C[de.a]
    E[e.c]-->|gcc -c|F[e.o]-->|ar -crv|C
Loading

d.c

#include "d.h"

extern int e_abs(int a);

int sum(int a, int b)
{
    return a + b;
}

int sub(int a, int b)
{
    return a - b;
}

int abs(int a)
{
    return e_abs(a);
}

e.c

#include "e.h"

int e_abs(int a)
{
    if (a >= 0) {
        return a;
    } else {
        return -a;
    }
}
  • 编译: d.c和e.c文件 aarch64-linux-gnu-gcc -c e.c d.c
  • 生成静态库文件:aarch64-linux-gnu-ar crv ed.a e.o d.o

3.2 静态链接库文件解压

flowchart LR
    C[de.a]-->|ar -t|B[d.o]
    C[de.a]-->|ar -t|F[e.o]
Loading

aarch64-linux-gnu-ar -t ed.a

3.3 使用静态链接文件编译

两个方法:

  • 使用ar -t解压成o文件,然后进行链接
  • 直接使用gcc进行编译链接aarch64-linux-gnu-gcc f.c ed.a -I ./ -L ./ -o f.elf

关闭内置优化aarch64-linux-gnu-gcc f.c ed.a -I ./ -L ./ -o f.elf -fno-builtin

4. TIPS

4.1 内建函数3

内建函数,顾名思义,就是编译器内部实现的函数。这些函数跟关键字一样,可以直接使用,无须像标准库函数那样,要 #include 对应的头文件才能使用。内建函数的函数命名,通常以 __builtin 开头。这些函数主要在编译器内部使用,主要是为编译器服务的。内建函数的主要用途如下。

4.2 -fno-builtin编译选项4

aarch64-linux-gnu-gcc f.c ed.a -I ./ -L ./ -o f.elf -fno-builtin

Ref

Footnotes

  1. 3.1 Basic Linker Script Concepts

  2. usage - virtual address translation method with neat diagram

  3. 嵌入式C语言自我修养 (11):有一种函数,叫内建函数

  4. Gcc编译选项-fno-builtin -fno-builtin-function

06_ARMv8_指令集_一些重要的指令

06_ARMv8_指令集_一些重要的指令

  • PC相对地址加载指令: ADR, ADRP
  • 内存独占加载和存储指令:LDXR, STXR
  • 异常处理指令:SVC, HVC, SMC (不包含在本期内)
  • 系统寄存器访问指令:MRS, MSR
  • 内存屏障指令:DMB, DSB, ISB

1. PC相对地址加载指令

1.1 指令ADR

相对于PC地址加一个立即数写入目标寄存器,Xd = PC + imm,得到偏移imm的PC地址的地址。实际上,执行的ADD/SUB 对PC地址的指令1

  • Define: ADR <Xd>, <label> . Note, 的范围是 ±1MB。
  • Example1: adr x1, #0xff -> 当前PC值,加上0xff写入x1

这里一直强调一个相对PC,这个相对这个词用的十分有讲究,具体参考,[1.4 ADR和LDR的陷阱](#1.4 ADR和LDR的陷阱).

1.2 指令ADRP

ADRP首先找到PC向下4K对齐的位置(寻找4K对齐的基地址)1,然后加上给定的赋给Xd寄存器。寻找4K向下地址可以给值的低12位(2^12=4096)清零,就可以了,这个实现可以参考链接的例子2

  • Define: ADRP <Xd>, <label> . Note, 的范围是 ±4GB。
  • Example1: adrp x1, #0xff -> 当前PC值->找到4k对齐的基地址->加上0xff写入x1。

1.3 Example

1.3.1 对比LDR和ADR指令

新建一个汇编文件,在汇编代码中定义一个my_test_data的标签

.align 3
.global my_test_data
my_test_data:
	.dword 0x12345678abcdabcd
  • 使用ADR和ADRP指令来读取.my_test_data的地址以及该地址的值
  • 请使用LDR指令读取.my_test_data的地址及该地址的值

【分析】:ADR和ADRP指令读取.my_test_data的地址,函数的地址势必是PC执行的地址,因此地址必须和PC关联,因此,标签自身的值+PC的值就应该是.my_test_data的地址,ADR x1, my_test_datal, 接着使用LDR x2, [x1]把x1寄存器地址里面的值加载到X2寄存器。x2的值应该是.dword的值

.global test_adr
.align 3
.global my_test_data

my_test_data:
	.dword 0x12345678abcdabcd

test_adr:
	adr x1, my_test_data
	adrp x2, my_test_data
	// read back offset
	add x2, x2, #:lo12:my_test_data
	ldr x3, [x1]

	// using ldr read label
	// my_test_data -> x4
	ldr x4, =my_test_data
	// *my_test_data -> x4
	ldr x5, my_test_data

	ret

1.3.2 页地址加载

修改链接文件linker.ld,在树莓派的4MB内存地址上分配一个4096大小的页面init_pg_dir,用来存储页表。请使用adrp和ldr指令来加载init_pg_dir的地址到通用寄存器。

  • 创建4096大小的init_pg_dir, 在linker.ld文件中SECTIONS括号内部输入:

    	. = 0x400000,
    	init_pg_dir = .;
    	. += 4096;
    
  • 在汇编代码里面直接读取该符号

    	// read init_pg_dir address
    	adrp x6, init_pg_dir
    	ldr x7, =init_pg_dir

    Note, 这里必须使用adrp,如果使用adr,会收到下面的错误信息asm_test.S:22:(.text+0x20): relocation truncated to fit: R_AARCH64_ADR_PREL_LO21 against symbol init_pg_dir defined in .rodata section in build/benos.elf 。原因是,init_pg_dir可输入的范围是1MB(0x100000),现在是4MB位置(0x400000),ADR无法访问到这个地址。

1.4 ADRP和LDR的陷阱

从上面的[example2](#1.3.2 页地址加载),似乎可以得到ADR和LDR可以通用的结论,LDR可以访问64bit整个地址空间的加载,但是ADR可以访问±4GB的地址空间,ADR为什么还有存在的必要呢?实际上这里涉及ELF文件的VMA和LMA的一个知识(在 03_ELF文件_静态链接的2.2 两步链接(Two-pass Linking),提到了VMA和LMA的概念,里面虚拟地址和物理地址在某些嵌入式系统里面可能会不一样),我们在树莓派的BOOTROM场景下,若把程序加载到0x8_0000的地址外运行,此时就会出现一个问题。

  • 若init_page_dir没有被MMU重定位,那么使用ldr和adrp指令能得到一个相同的结果。
  • 若init_page_dir有被MMU重定位,那么使用ldr和adrp指令就会得到不同的结果,你会发现,使用adrp指令会找当前的PC值加上偏移,因此还是在运行地址范围内;使用ldr指令加载的是VMA的地址,会得到被MMU重定位的地址。
  • PC永远都是在运行地址之内的,所以看到这个”相对的“这个词还是很有讲究的。
  • 树莓派的BOOTROM下,如果初始化了MMU页表操作之后,adrp和ldr的使用就会出现问题。

image-20220324131142609

我们现在制造一个LMA和VMA不同的情况,以研究ADRP和LDR的差别,基于上面的[example2](#1.3.2 页地址加载):

  • 修改link.ld文件,使整个区域被映射到0xFFFF_0000_0008_0000高地址上,此时被编译出来的elf文件的链接地址全部都被放到高地址上。

    SECTIONS
    {
    	. = 0xFFFF000000080000,
    	.text.boot : { *(.text.boot) }
    	.text : { *(.text) }
    	.rodata : { *(.rodata) }
    	.data : { *(.data) }
    	. = ALIGN(0x8);
    	bss_begin = .;
    	.bss : { *(.bss*) }
    	bss_end = .;
    
    	. = ALIGN(4096),
    	init_pg_dir = .;
    	. += 4096;
    }
  • 在调试的时候使用GDB调试手段,add-symbol-file3强制使ELF文件在0x8_0000地址运行,此时PC也在这个范围内。aarch64-linux-gnu-readelf -S benos.elf

    Section Headers:
      [Nr] Name              Type             Address           Offset
           Size              EntSize          Flags  Link  Info  Align
      [ 1] .text.boot        PROGBITS         ffff000000080000  00010000
           0000000000000030  0000000000000000  AX       0     0     4
      [ 2] .text             PROGBITS         ffff000000080030  00010030
           00000000000002b8  0000000000000000  AX       0     0     8
      [ 3] .rodata           PROGBITS         ffff0000000802e8  000102e8
           000000000000001c  0000000000000000   A       0     0     8
    

    在GDB加载符号之前使用add-symbol-file benos.elf 0x80030 -s .text.boot 0x80000 -s .rodata 0x802e8 ,把.text.boot, .text, .rodata段强制替换到树莓派可以运行的地址上面。

  • 使用ldrp指令访问的x2, x1寄存器都是在GDB使用的PC(LMA),而x4和x7使用LDR指令的加载的都是链接地址也就是VMA。

image-20220324135231019

2. 内存独占加载和存储指令

在介绍内存独占加载和存储指令之前,先科普一下ARMv8架构里面的一个机制-独占监视器(Exclusive monitor)4 ,虽然这个这个是ARMv6比较老的架构上面的文章,但是这个原理是不变的。ARM里面有两个独占监视器,一个本地的独占监视器,还有一个是全局的独占监视器。本地的独占监视器用于监视non-shareble/shareble的地址访问,全局的独占监视器用于监视shareble的地址访问(多核,如图Cortex-A8/Cortex-R4)。LDXR指令会让监视器进入到独占状态,STXR存储只有当独占监视器还处于独占状态的时候才可以存储成功。

image-20220324142522170

实际上内存独占和加载指令为操作系统的一些原子操作提供底层的技术支持,Linux内核一些atomic的访问,比如atomic_write(), atomic_set_bit()的这些原子操作在底层的指令都有涉及到内存独占。这里有个文章可以参考,spinlock上面如何应用LDXR, STXR5.

2.1 指令LDXR

内存独占加载指令。以内存中独占exclusive的方式加载内存地址到通用寄存器。

  • Define: LDXR <Xt>, [<Xn|SP{, #0}>]
  • Example1: ldxr x1, sp -> 当前sp指针独占地加载到x1寄存器

2.2 指令STXR

内存独占存储指令。

  • Define: STXR <Ws>, <Xt>, [<Xn|SP{, #0}>]
  • Example1: stxr w0, x1, sp -> 独占的把x1的内容写入到sp内,写入结果放在w0寄存器,w0为0表示写入成功,w0为1表示写入失败。Note, w0是一个32位的寄存器。

2.3 Example

2.3.1 实现atomic_write函数

使用汇编实现atomic_write函数,在汇编定义数据my_data,初始化为0,然后使用atomic_write来写入my_data的这个数据atomic_write(0x34),使用C语言调用这个函数测试。

这个C语言代码:

int  my_data = 0;
int atomic_write(int a)
{
  my_data = a;
  return a;
}

ASM:

.section .data
.align 3
.global my_test_data
my_test_data:
	.dword 0
.section .text
.global my_atomic_write
my_atomic_write:
	// get my_test_data addr atomicl
	ldr x2, =my_test_data
1:
	ldxr x1, [x2]
	orr x1, x1, x0
	// save x0 to x2, the result on w0
	stxr w0, x1, [x2]
	cbnz w0, 1b
	mov x0, x2
	ret

Note, 汇编里面要映射.data和.text区域,否则在某些环境会报段错误。

3. 系统寄存器访问指令

对于系统寄存器的访问不能像是通用寄存器一样,系统寄存器非常特殊,所以就需要特殊的指令进行访问。

  • MRS
    • Define: MRS <Xt>,(<sestem_reg>|S<op0>_<op1>_<Cn>_<Cm>_<op2>)
  • MSR
    • Define1: MSR <pstatefiled>,#<imm>
    • Define2: MSR (<sestem_reg>|S<op0>_<op1>_<Cn>_<Cm>_<op2>), <Xt>

4. 内存屏障指令

内存屏障指令DMB, DSB还有ISB指令

  • DMB (Data Memory Barrier)6

    保证内存屏障前后的内存访问指令的执行顺序

    • Define: DMB <option>|#<imm>
  • DSB (Data synchronization Barrier)7

    任何执行都要等待DSB前面的存储访问完成

    • Define1: DSB <pstatefiled>,#<imm>
  • ISB (Instrution synchronization Barrier)8

    冲洗流水线和预取buffer,才会从高速缓存或者内存中预取ISB指令之后的指令

    • Define1: DSB <pstatefiled>,#<imm>

Ref

Footnotes

  1. 汇编七、ADRP指令 2

  2. test_bits.c: test_4k_align_using_the_clear

  3. GDB Maunal - 18.1 Commands to Specify Files

  4. ARM Synchronization Primitives Development Article - Exclusive monitors

  5. Exclusive monitor在spinlock中的应用 - Cache One

  6. Arm Armv8-A A32/T32 Instruction Set Architecture - DMB

  7. Arm Armv8-A A32/T32 Instruction Set Architecture - DSB

  8. Arm Armv8-A A32/T32 Instruction Set Architecture - ISB

Linux内核调用I2C驱动_驱动嵌套驱动方法MPU6050

Linux内核调用I2C驱动_以MPU6050为例

0. 导语

最近一段时间都在恶补数据结构和C++,加上导师的事情比较多,Linux内核驱动的学习进程总是被阻碍、不过,十一假期终于没有人打扰,有这个奢侈的大块时间,可以一个人安安静静的在教研室看看Linux内核驱动的东西。按照Linux嵌入式学习的进程,SPI驱动搞完了之后就进入到I2C驱动的学习当中,十一还算是比较顺利,I2C的Linux驱动完成了。

为了测试I2C是否好用,选择一个常用的I2C传感器,手头有个MPU6050,刚好作为I2C的从器件,那就以MPU6050为例,进行Linux底层的I2C驱动开发。

同样的使用Linux内核中的GPIO模拟I2C的时序一点难度没有,I2C的硬件标准时序也是非常的简单,闭着眼睛都能画出时序图吧,如果我们使用Linux内核提供了I2C机制,那么问题不单单是要解决时序,而重点在于对于整个I2C的机制的把握,,。刚刚拿到I2C内核机制的时候,我也看的很晕,i2c_client, i2c_master, i2c_driver, i2c_device,这些东西到底有什么关系呢?到底我该如何让Linux系统的I2C为我所用,按照我的意愿对MPU6050进行读取?到底我能挑出对我有用的Linux的I2C机制,其他没用的机制我不启动,以简化代码。

那么,就真需要从I2C最底层说起。

1. 实验平台

平台 内容
ARM板子 友善之臂Nano-T3 (CortexA53架构,Samsung S5C6818芯片)
ARM板子的Linux系统 Ubuntu 16.04.2 LTS
Linux开发主机 Ubuntu 16.04.3 LTS amd4版本
Linux内核版本 Linux3.4.y
编译器COMPILE_CROSS arm-cortexa9-linux-gnueabihf-
从设备 MPU6050模块(I2C接口)

2. 查看系统I2C的支持

按照SPI驱动的思维,使用spi_driver注册,然后和spi_device匹配,使之进入probe函数,完成spi_master的获取,依照这个方法,我的I2C驱动也是按照这个方法,寻求i2c_driver和i2c_device匹配,然而I2C的驱动尤其特殊之处,使得我的i2c_driver怎么注册都不成功,不是内核内存炸了,就是总是返回失败。

后来我才发现,i2c的使用是不需要注册的,或者严格说一点,Linux系统在启动的时候已经帮你注册好了,而你再去i2c_driver_register的时候肯定是失败的。**所以到底我们使用I2C驱动的时候到底需不需要注册,则需要在Linux系统里面查看当前I2C的注册状态。**那么流程就比较清晰了,如果查看系统注册了I2C那么就在驱动中直接使用;如果系统没有注册I2C那么我们先注册I2C再使用。

2.1 如何查看?

目标板终端输入:ls /sys/bus/i2c/devices

可以看到我这个主机是支持4个I2C外设的(方框圈出),如果是这样的情况,我们就可以直接使用上面的i2c。这里的i2c-0,i2c-1....指的是4个i2c_master,而i2c_master可以挂N个i2c_client

其他的数字设备就是我挂载的i2c_master上的i2c_client,举个例子,画圈的【0-0069】意思是:挂载到i2c-0上的从地址为0x69的设备,那么【2-0048】的意思就是:挂载到i2c-2 adapter上的从地址为0x48的设备。

我们开发的MPU6050驱动依托I2C进行传输,则需要在这个文件夹创建设备节点才能利用Linux内核提供的I2C方法进行数据的交互。

2.2 弄清楚MPU6050的从地址与Linux I2C从地址的合法性

随手搜了一下MPU6050的从地址,有的给出了MPU6050的从地址是0x68,有的给出的是0xD0,一开始我也懒查,认定MPU6050的地址在A0引脚为低电平的时候为0x68,加载驱动的时候出现了很尴尬的事情,0-0068这个地址已经被DS1607实时时钟占用,然后网上有人说是把A0引脚打到高电平地址就是0xD0,可是我试0xD0的时候,被Linux警告,说是从地址不合法,我查看了Linux内核的i2c_core.c文件,里面有个地址校验,高于0x7F的7-bit地址,都是不合法的,Linux不可能犯这样的错误,肯定是网友的锅。果然,我阅读了手册,如果A0的电平为高那么地址是0x69。说从地址是0xD0的人,犯了一个错误,他们多半玩的是模拟IO出的I2C波形,他们对I2C协议标准不够了解,的确0x69 << 1 = 0xD0,I2C在读写的时候,预留出7-bit地址前移1位,把最低位作为读写标识,但绝对不能说从地址就是0xD0。

不过可以再一次看见Linux内核的严谨、严肃的态度。也再一次说,不能懒惰,自己查手册,看最标准的说明。

3 I2C 驱动开发

我这里给出最简单的模型,其他的字符驱动注册什么的同spi驱动,这里只说明I2C驱动怎么使用。

3.1 I2C的注册

static struct i2c_board_info __initdata sp6818_mpu6050_board_info = {
	I2C_BOARD_INFO("mpu6050-i2c", MPU6050_SLAVE_ADDRESS),,
	.irq	= -1,
};

int xxx_hw_init(){
	struct i2c_client *client;
	struct i2c_adapter *adapter;

	adapter = i2c_get_adapter(0);
	if (!adapter) {
		ret = -ENXIO;
		printk(DRV_NAME "\terror: %d : init i2c adapter failed.\n", ret);
		return ret;
	}
	strlcpy(adapter->name, "nxp_i2c",sizeof(adapter->name));
	client = i2c_new_device(adapter, &sp6818_mpu6050_board_info);
	if (!client) {
		ret = -ENXIO;
		printk(DRV_NAME "\terror: %d : init i2c client failed.\n", ret);
		return ret;
	}
}

你没有看错,i2c的使用就是这么简单,我有什么办法,我之前开发加上i2c_register和字符驱动的初始化什么的,整init函数整了近100多行,结果不断的尝试,发现就这些。

下面就说几个重点:

3.1.1 adapter的获取

adapter = i2c_get_adapter(0);定义一个指针,然后使用i2c_get_adapter(0),得到我们上面说的,i2c-0,这个adapter。你疑问了,我为什么选择i2c-0这个adapter,为什么不选择-i2c-其他。因为这个开发板只把-i2c-0的引脚印出来了。

。。。

这样就获取到了adapter。

3.1.2 client的创建

接着我们就要创建一个client,这个client就指的是你的mpu6050,我们使用i2c_board_info这个结构体来描述mpu6050,先定义一个这个info:

static struct i2c_board_info __initdata sp6818_mpu6050_board_info = {
	I2C_BOARD_INFO("mpu6050-i2c", MPU6050_SLAVE_ADDRESS),,
	.irq	= -1,
};

第二行的,”mpu6050-i2c“就是注册到Linux系统里面的设备名字,可以在如图所示路径和cat命令查看。

MPU6050_SLAVE_ADDRESS就是MPU6050的地址了,0x69 ,MPU6050的A0接高电平,地址是0x69没毛病。

然后,就是生成这个client且和之前那个adapter绑定:

client = i2c_new_device(adapter, &sp6818_mpu6050_board_info);

之后client的信息和adapter的信息我们要保存起来,可以定义一个全局指针之类的承接初始化后的client和adapter,因为后面的传输数据要用。

到此,I2C完成了,很简单,可是探索起来好麻烦。

3.2 I2C数据的写入

static int 
__mpu6050_write_reg(MPU6050* this, char reg_addr, char reg_value)
{
	int ret;
	struct i2c_msg msg;
	char write_buffer[10];
	
	memset(write_buffer, 0, 10);
	write_buffer[0] = (char)reg_addr;
	write_buffer[1] = (char)reg_value;
	msg.addr = (this->hw->i2c_clit->addr);
	msg.flags = 0;
	msg.len = 2;
	msg.buf = &write_buffer[0];
	ret = i2c_transfer(this->hw->i2c_adper, &msg, 1);

	return ret;
}

看一下我的数据写入函数,提取出有用的信息,mpu6050写寄存器,需要传输两个字节的信息,一个是寄存器地址,另一个是寄存器的值,按照上面的格式进行,msg.length不包含器件的从地址,就是实在的你想法几个数据的多少,我们这里只发两个,一个是寄存器地址和寄存器的值,所以是2;如果你是要发送则msg.flag一定是0。

我的函数this->hw->i2c_adapter就是上面存储的adapter的指针,this->hw->i2c_clit就是存储的上面初始化的client的指针。

3.3 I2C数据的读

读相比于写就费劲多了,但是也没难到哪里去,只不过是两条msg,先写后读:

__mpu6050_read_reg(MPU6050* this, char reg_addr)
{

	struct i2c_msg msg[2];
	char write_buffer[10];
	int ret, i;

	memset(write_buffer, 0, 10);
	memset(this->buffer, 0, 10);
	write_buffer[0] = (char)reg_addr;
	msg[0].addr = (this->hw->i2c_clit->addr);
	msg[0].flags = 0;
	msg[0].len = 1;
	msg[0].buf = &write_buffer[0];
	msg[1].addr = (this->hw->i2c_clit->addr);
	msg[1].flags = I2C_M_RD;
	msg[1].len = 1;
	msg[1].buf = &this->buffer[0];
	
	ret = i2c_transfer(this->hw->i2c_adper, &msg, 2);
}

意思很明显。

到此,i2c的注册和数据传输完成,我们可以在上层建立函数读取MPU6050的值了。

4 成果

验证函数:

#include <unistd.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>

short x_accel, y_accel, z_accel;
short x_gyro, y_gyro, z_gyro;

int main()
{
	char buffer[128];
	short *time;
	int in, out;
	int nread;
	
	in = open("/dev/MPU6050", O_RDONLY);
	if (!in) {
		printf("ERROR: %d, Open /dev/MPU6050 nod failed.\n", -1);
		return -1;	
	}	
	nread = read(in, buffer, 12);
	close(in);	
	if (nread < 0) {
		printf("ERROR: %d, A read error has occurred\n", nread);
		return -1;	
	}

	time = (short*)buffer;
	x_accel = *(time);
	y_accel = *(time + 1);
	z_accel = *(time + 2);
	x_gyro =  *(time + 3);
	y_gyro =  *(time + 4);
	z_gyro =  *(time + 5);
	printf("x accel is: %d \n", x_accel);
	printf("y accel is: %d \n", y_accel);
	printf("z accel is: %d \n", z_accel);	
	printf("x gyro is: %d \n", x_gyro);
	printf("y gyro is: %d \n", y_gyro);
	printf("z gyro is: %d \n", z_gyro);

	exit(0);
}

测试脚本:

# !/bin/bash                                                                            
for((i=1;i<=10000;i++));                                                                
do                                                                                      
./test_mpu6050.o                                                                        
sleep 1                                                                                 
done                                                                                    

源代码:

Github地址:https://github.com/lifimlt/carlosdriver

见 mpu6050.c mpu6050.h 和mpu6050_def.h三个文件

mpu6050_test.c为测试文件

参考文献:

[1] Linux org, Serial Peripheral Interface (I2C),

[2] choiyoung87, Linux中的I2C(二)——adapter的初始化, 2011年12月01日

[3] liuwanpeng , [《linux设备驱动开发详解》笔记——15 linux i2c驱动](http://www.cnblogs.com/liuwanpeng/p/7346558.html), 2017年8月23日

03_ARMv8_指令集介绍_加载指令集和存储指令集

Github地址:carloscn/uncle-ben-os at car_lab_06 (github.com)

ARMv8指令集介绍

  • A64指令集只能运行在aarch64
  • 所有A64汇编都是32 bits宽的
    • 关注指令的使用、有什么limitation
    • A64能访问的地址数据是64位宽的
  • A64支持全部的大写或者小写方式
    • ARM官方大写
    • 应用使用小写
  • 寄存器命名
    • Wn表示32bits宽的寄存器
    • Xn表示64bits宽的寄存器
    • WZR表示32位内容全为0的寄存器
    • XZR表示64位内容全为0的寄存器
    • ...

LDR指令

LDR Xd, [Xn, $offset]

  • 【释义】:将Xn寄存器中存储的地址+offset地址偏移存 组成一个新的地址,把这个地址里面存储的值放在Xd寄存器中。[]有取地址内存储的数值的含义。

  • 【示例】:

    • S1: 使用MOV指令把0x80000加载到X1寄存器: MOV x1, 0x80000 (如果是一个数,而非#0x80000, 则是一个地址)

    • S2: 使用MOV指令把16数值加载到X3寄存器: MOV x3, 16

    • S3: 使用LDR指令读取X1地址里面存储的值,存储到X0中: LDR x0,[x1] , 这个不允许->LDR x2,[0x80000]

    • S4:使用LDR指令读取X1 + 8地址里面存储的值,存储到X2中:LDR x2,[x1, #8]

    • S5:使用LDR指令读取(X1 + X3)地址里面存储的值,存储到X4中: LDR x4,[x1, x3]

    • S6: 使用LDR指令读取(X1+(X3<<3))地址里面存储的值,存储到X5中: LDR x5,[x1,x3,lsl #3]

  • 【注意】:

    • 给的数不加任何标志的视为地址
    • 需要给立即数的场景而非地址的值,使用#
    • []有取地址值的意思
    • LDR lsl扩展指令,只支持1和3
    • LDR x2,[x1, #8] x1的值不会被更新为0x80008
  • 【变基模式】:

    • 前变基模式 pre-index: 先更新偏移地址,后访问地址 (注意有叹号!表示)

      LDR x6, [x1, #8]! : 将x1里面的地址增加偏移#8并赋给x1,最后将新的x1寄存器内的地址的值给x6寄存器

    • 后变基模式 post-index: 先访问内存地址,再更新偏移地址

      LDR x6, [x1], #8 : 将x1寄存器内的地址的值赋给x6寄存器,并将x1地址偏移+8。

  • 【伪指令】:

    伪指令与指令的最大不同在于,伪指令属于编译器处理的范畴,伪指令会被编译展开为多条指令;指令是CPU处理的命令的最小单元。

    • LDR x7,=0x80000 -> 等同于 MOV x7, 0x80000
    • 需要区别 LDR x7, 0x800000; 这条指令的意义是,将当前PC寄存器的地址的 + 0x80000的偏移,取出地址内容填充到x7寄存器中。

STR指令

从一个寄存器的值吐到内存中,支持立即数和寄存器操作。把Xd的值,存储到[xn|sp]里面。

immediate-post-index: STR Xd, [Xn|SP], #<simm>

immediate-pre-index: STR Xd, [Xn|SP, #<simm>]!

  • 【示例】:

    ; Example 1:
    MOV x2, 0x400000             ; -> x2 is 0x400000
    LDR x6, =0x1234abce          ; -> x6 is 0x1234abce
    STR x6, [x2, #8]!            ; -> 把x6的值(0x1234abce),存储到0x400008地址的内存里面
    ; What's value of x2? And the value in 0x400000 address?  
    
    ;Example 2:
    MOV x2, 0x500000			 ; -> x2 is 0x500000
    STR x6, [x2], #8			 ; -> 把x6的值(0x1234abce),存储到0x500000里面,并将x2寄存器变为0x500008
    ; What's value of x2? And the value in 0x400000 address?  

MOV/MOVZ指令

MOV底层原理实际上是MOVZ,MOV 16-bit的立即数到寄存器。

MOV xd, #<imm> 16位立即数

MOVZ xd, #<imm16>, LSL #<shift> 16位的立即数,逻辑左移动 16,32,48位

LDP/STP指令

相比于LDR和STR指令(8 bytes),LDP和STP指令用于多字节(16 bytes)操作,

【释义】:

  • LDP :LDP x3, x7, [x0] -> 从x0的值为基地址,加载地址到X3寄存器,存储x0+8到x7寄存器。
  • STP :STP x1, x2, [x4]-> 以x4的值为基地址,存储x1地址的值到x4,存储x2地址的值到x4 + 8。

【练习】:

练习1: 使用LDR和STR多字节加载和存储命令实现memset()函数,假设内存地址s是16字节对齐,count也是16字节对齐。例如:memset(0x200000, 0x55, 32)

// memset_a_byte
void *memset_a_byte (void *s, int c) {
    char *xs = s;
    *xs++ =c;
    return s;
}
// 使用STR指令,单字节操作
.global my_memset_test:
my_memset_test:

// 保存地址s到x1寄存器,保存c的值到x2寄存器,保存长度到x3寄存器
MOV x1, 0x2000000   // 这个值是需要被修改的 肯定需要STP
MOV x2, 0x55        // 这个是个固定的参数
ADD x0, x1, 32
// 确定原子操作 向地址写值,然后地址增加
wrt:
STR x2, [x1], #8	// 把x2里面的值存储到x1里面(0x55 -> 0x200000),接着0x200008加一
cmp x1, x0
b.cc wrt

ret
// 使用STP指令,双字节操作
.global my_memset_test:
my_memset_test:

// 保存地址s到x1寄存器,保存c的值到x2寄存器,保存长度到x3寄存器
MOV x1, 0x2000000   // 这个值是需要被修改的 肯定需要STP
MOV x2, 0x55        // 这个是个固定的参数
ADD x0, x1, 32
// 确定原子操作 向地址写值,然后地址增加
wrt:
STP x2, x2, [x1], #16	// 把x2里面的值存储到x1里面(0x55 -> 0x200000),接着0x200008加一
cmp x1, x0
b.cc wrt

ret

练习二:同上,使用非对齐的memset(0x200004, 0x55, 37)

// 需要汇编和C语言混合编程实现对于非16字节对齐的地址和长度进行memset操作
// 汇编实现一个16字节的memset
// C语言用于对非对齐部分进行C语言单字节的处理,用汇编实现16字节对齐地址和16字节对齐长度的处理。

// 函数调用为 memset(0x200004, 0x55, 37)
.global asm_memset_16_byte_align:
asm_memset_16_byte_align:
ADD x4, x0, x2
wrt:
STP x1, x1, [x0], #16
CMP x0, x4
b.cc wrt
ret

void *memset (void *s, int c, int count)
{
   int align = 16;
   if (s & (align - 1)) {
     // 处理非对齐地址
   }
   // 对齐部分直接调用 asm_memset_16_byte_align(s, c, l);
   // 非对齐部分直接c语言指针访问赋值。
}

一些需要注意的地方

  • FAQ1:加载一个很大的数值到通用寄存器,例如0xFFFF0000FFFF0000, 使用MOV指令,是否正确?

    错误,MOV 后面的立即数为16-bit,应该是使用LDR x1,=0xFFFF......0000 伪指令来加载大数。

  • FAQ2:加载一个寄存器的值,使用移位:MOV x1, (1<<0) | (1<<2)|(1<<20)|(1<<40)|(1<<55)

    错误,同样是MOV立即数16-bit,使用LDR x1, = (1<<0)|......|(1<<55).

  • FAQ3: 字符串的LDR指令

    string1:
    	.string "Booting at EL"
    LDR x0, string1      // 加载string1字符串的ascii码值到寄存器,最高限制在X寄存器大小也就是 64-bit,如果是W寄存器就是32-bit    
    LDR x1, =string1     // 加载string1字符串的地址到x1
    
  • FAQ3: 定义数据LDR指令

    my_data:
    	.word 0x40
    LDR x0, my_data     //加载0x40到X0,等同于MOV x0,0x40 前提是不超过16bits
    LDR x1, =my_data    //加载存储my_data的地址到x1
    
  • 一种易错的死机状态: 树莓派4b上面的寄存器都是32bit的,下面代码配置26到U_IBRD_REG寄存器,有什么问题?

    LDR x1, =U_IBRD_REG
    MOV x2, #26
    STR x2, [x1]
    
    //正确写法:
    LDR w1, =U_IBRD_REG
    MOV w2, #26
    STR w2, [w1]
    

    错误点在于树莓派4b寄存器访问都是32bit的,现在使用X寄存器,为64位的寄存器,应该使用W寄存器,32位寄存器访问。

GDB-Tips

  • 启动GDB和QEMU链接

    • > gdb-multiarch --tui benos.elf

    • gdb> c

    • gdb> target remote localhost:1234

    • gdb> b ldr_test // 设定断点

    • gdb> c

    • gdb> next //下一步

    • gdb> info register // 查看所有寄存器

    • gdb> info x1 x2 x3 // 查看x1/x2/x3寄存器

    • gdb> x 0x80000 // 读取内存0x80000值 32位

    • gdb> x/xg 0x80000 // 读取内存0x80000值64位

Addressing

参考03_ARMv7-M_存储系统结构的地址对齐访问设计。我们对ARMv8架构的对齐操作进行整理。

和Cortex-M一样,独占和顺序(ordered)访问(相对于指令预取和乱序访问)不可以对非对齐的地址进行访问。但是所有的load和store是支持非对齐访问的

块传输(bulk transfers)

  • 这些 LDMSTMPUSH, 和 POP指令不存在与A64指令集。所有的块传输都是通过STPLDP
  • The LDNP and STNP instructions provide a streaming or non-temporal hint, that the data does not need to be retained in caches. 指令提供了一个流式或非临时提示,即数据不需要保留在缓存中。
  • The PRFM, or prefetch memory instructions enable targeting of a prefetch to a specific cache level .“PRFM”或预取存储器指令能够将预取定为特定的高速缓存级别。

load/store

  • All Load/Store instructions now support consistent addressing modes。现在,所有加载/存储指令都支持一致寻址模式。This makes it much easier, for example, to treat charshortint and long long in the same way when loading and storing quantities from memory. 例如,这使得在从内存加载和存储量时更容易以相同的方式处理“char”、“short”、“int”和“long-long”。
  • 浮点寄存器和NEON寄存器现在支持与核心寄存器相同的寻址模式,从而更容易互换使用这两个寄存器组。

Alignment checking

当执行在AArch64模式的时候,需要对取指令的load和store操作需要使用栈指针,所以会对PC和SP进行对齐检查。

https://developer.arm.com/documentation/den0024/a/An-Introduction-to-the-ARMv8-Instruction-Sets/The-ARMv8-instruction-sets/Addressing

How to compile mbedtls library on Linux/Mac/Windows

How to compile mbedtls library on Linux/Mac/Windows

[Brief] mbedtls library is arm security suite for embedded device. It can be MAKE on multi-platform as host linux, mac os even windows OS. This paper guide you configuring and compiling the mbedtls library on Linux/Mac/Windows OS and link the output binary library files on your C project.

1 Code Prepare

MKDIR a path for saving your code. I named it work and cd work path. Using the git clone function to pull the newest mbedtls code as follows cmd.

git clone https://github.com/ARMmbed/mbedtls.git

2 Env Prepare

As a Linux user you need prepare gcc compiler.

As a Windows user I recommend you using the mingw compiler.

As a Mac user you can use same as linux gcc.

3 Compile Code

linux and mac user : make SHARED=1

and sudo make install

that is different from linux and mac user:

set WINDOWS=1 mingw32-make CC=gcc

windows mingw user: mingw32-make CC=gcc SHARED=1 -j8

4 Detail the Path

linux and mac user please find library files and include path in /usr/local/lib and /usr/local/include you can find

libmbedcrypto.so

libmbedtls.so

libmbedx509.so

and related .a file.

windows mingw user need copy the library and include path or files on compiled path.

10_ARMv8_异常处理(一) - 入口与返回、栈选择、异常向量表

10_ARMv8_异常处理

终于,我们从ARMv8的一些汇编指令的学习当中出来了,开始研究ARMv8体系架构的设计。这里面的机制十分的重要,所以不得不依赖笨叔的视频,同样自己也要阅读ARMv8的手册,希望自己一字一句的去理解,不懂的去搜索,争取把这部分知识补充完整。

1. ARMv8 fundamentals1

这部分在笨叔的视频和讲义里面只是粗略的讲了一下high-level的版图设计,但我觉得这个十分重要,没有这个基础后面很难去理解异常处理的过程,而且在high-level上面,人为的设定了很多规则,我觉得有必要整理一下。大纲总结如下:

image-20220412174235416

1.1 EL/PL/secure/non-secure

这里主要提到了一些概念和术语,我们需要理解这些概念还背后术语的一些潜在的设计意义,挖掘出设计者的意图。文档在这一章开篇就提到:异常等级(Exception level)、特权等级(Privilege level)。ARMv8内部包含4个异常等级:

  • EL0:普通用户的应用程序
  • EL1:典型的操作系统内核,这个等级被称为“特权等级”(privileged)
  • EL2:hypervisor
  • EL3:Low-level固件,例如安全监视器 Secure monitor,最高权限

这里就有些需要注意的了,特权等级是在ARMv7里面的概念,ARMv8属于借鉴过来,PLn对应ELn。

ARMv8提供了两个安全状态(Secure States),一个叫普通世界(normal world),一个叫安全世界(secure world),这两个世界并行的运行在同一个硬件设备上面,安全世界的工作重心是抵抗软件和硬件的攻击。在EL3下的secure monitor,游走于secure world和normal world。

image-20220412152247152

在normal world的hypervisor(VMM)代码运行在系统上并且管理着多个Guest OS,所以每一个操作系统都运行在VMM上面,每个操作系统在同一时间都不知道有其他操作系统在运行。

  • Guest OS kernels,这部分分为两类:

    • 一般性的操作系统Linux/Windows
    • 在运行hypervisor的情况下,还有一个Rich OS kernels也作为Guest OS。我猜测,比如OPTEE-OS。
  • Hypervisor,总是normal world的,when present,主要是给rich os kernels提供服务。

  • Secure Firmware,这段程序必须运行在boot时间,用于初始化trusted os,平台,还有secure monitor。

  • Trusted OS,在EL1和Guest OS并行运行,提供一个runtime环境执行安全应用。OPTEE-OS

1.2 Execution States

ARMv8定义了两个执行状态,aarch64和aarch32。这两个执行状态和异常等级没有概念上面的交织,也就是aarch64和aarch32都有相应的异常等级和特权模式。在aarch64执行状态时,使用A64指令,在aarch32执行状态,使用A32或者T32(Thumb)指令集。

image-20220412153728651

AArch32模式,有一点不同的是关于trust-os的位置,trusted-os软件执行在secure EL3,在aarch64执行模式,是执行在EL1中的。ARMv7和aarch32很像。

1.3 Changing Exception levels

对于异常模式的变迁,ARMv8和ARMv72是一致的。异常模式的变迁不光是EL层级的变迁,在EL层级里面还有很多处理器的模式(mode)。这里有个术语叫做[take an exception],可以理解为异常的激发。当take an exception的时候,处理器的模式(mode)就会被改变。不同的EL层级内包含不同的模式集合。我理解这部分在ARMv8里面已经拿掉了,似乎没有这个概念,全部都被异常处理cover

Mode Function Securitystate Privilegelevel
User (USR) Unprivileged mode in which most applications run Both PL0
FIQ Entered on an FIQ interrupt exception Both PL1
IRQ Entered on an IRQ interrupt exception Both PL1
Supervisor(SVC) Entered on reset or when a Supervisor Call instruction (SVC) is executed Both PL1
Monitor (MON) Entered when the SMC instruction (Secure Monitor Call) is executed or when the processor takes an exception which is configured for secure handling.Provided to support switching between Secure and Non-secure states. Secure only PL1
Abort (ABT) Entered on a memory access exception Both PL1
Undef (UND) Entered when an undefined instruction is executed Both PL1
System (SYS) Privileged mode, sharing the register view with User mode Both PL1
Hyp (HYP) Entered by the Hypervisor Call and Hyp Trap exceptions. Non-secure only PL2

ARM64处理器内部的中断分为两种,FIQ和IRQ,FIQ叫快速中断请求,IRQ就是普通的中断请求。FIQ的优先级高于IRQ。

在ARM64处理器中异常模式有以下规则:

  • 升权:向EL3方向变迁,视为升权。
  • 异常不能被带入比其低的异常等级。
  • 在EL0没有异常处理机制,异常处理必须在其更高的异常等级处理。
  • 异常会造成程序执行流改变。通常异常处理有一个向量表需要循序,而有特例可以不需要遵循异常向量表:
    • IRQ/FIQ
    • abort memory system
    • 未定义指令
    • 系统调用
    • traps EL1和EL2
  • 异常处理结束之后返回前一个异常等级需要执行ERET指令
  • ERET之后只能保留在本层或者是更低的层级。
  • 安全状态无法改变EL层级。

1.4 Changing execution state

32位的应用程序可以运行在64位的上面,这个在x86的架构似乎也能看见。在ARM64处理器上也是支持这样的使用场景的。这样的执行是有个条件的,也就是在执行32位程序的时候,必须让处理器处于AArch32的执行状态。为了实现这样的执行,处理器只能在更高的EL层级去执行32位的程序。首先,32位的应用程序在EL0产生一个SVC指令向supervisor call,接着会产生一个IRQ,这个IRQ就会切换到AARCH64内的EL1,等着程序运行完毕之后任务ERET到EL0。32/64混合的程序没办法在ARM64上运行,还有64位程序就没办法在32位系统上运行了。

由于在执行32位程序的时候,处理器会升一个EL,因此即使AArch64状态能够支持AArch32,但是是在低一级的权利模式。一个AArch64的操作系统能够运行32/64两种类型的应用,hypervisor也是一样,AArch64的hypervisor能够管理AArch32和AArch64的guest操作系统。AArch32的并没有这个能力。

image-20220412171739974

2. ARMv8 exception handling

在ARM的世界我们应该纠正一个概念,“中断”。通用的中断的定义就是可以阻止正常软件流程执行的中断。然而在ARM的术语里面,不能仅仅用中断来表示中断,我们需要更上一层的定义,异常(Exception)。异常包含狭义的中断,也包含条件变量(conditions)或者是系统事件(system event),这些行为都有专门的异常处理函数来处理。在ARCH64里面有三类异常,中断(interrupts),中止(aborts) 和复位(reset)。

2.1 exception types

interrupts

在AArch64体系架构中有两种类型的中断,IRQ和FIQ。FIQ的优先级高于IRQ,这部分中断设计的时候我们再说。即便我们对于中断的概念已经很熟悉了,但这里面需要澄清几个有关中断的概念。

  • 中断线(request line)3:这个是对处理器而言的,是中断控制器的输入。
  • 中断向量表(vector table):中断名单,操作系统实现,内部包含中断线对应的IRQ号。

对于触发中断,在CPU上有专门的动词术语,assert一个中断,take一个异常。中断线由外部外设assert,中断线的信号被接入到中断控制器,中断控制器根据中断优先级判断先发出哪些信号,再根据中断向量表查找到中断处理函数的地址。由于中断并非执行在给定时间内,因此中断属于异步异常

aborts

同样包含两列aborts,取指失败(Instruction Aborts)或者是访问数据失败(Data Aborts)。

在内存上,发生这种异常的场景可能有两种,在访问内存时,外部存储器返回一个错误,或者指定访问的地址没有关联到真实的内存中(MMU产生该错误,一个操作系统可以使用MMU abort动态的分配内存给应用程序)。

在指令上,aborts发生在取指还未执行的时候。data abort发生在存储和加载指令。

abort异常属于同步异常

reset

reset异常有着最高的异常等级。当异常发生的时候ARM处理器就会跳转到指令所在的位置。RVBAR_ELn包含跳转地址,n应该是该处理器的最高权限,armv8里面就应该为n=3。该异常不可屏蔽,不可禁止,并且尝尝执行在上电后的初始化的时候。

Exception generating instructions

一些异常需要由CPU外部的触发中断线间接性的塑造异常,一些系统调用指令也可以直接的产生异常,这些指令通常会向处理器请求一些运行在更高权限环境中的服务。

  • 管理员权限请求(Supervisor Call, SVC):用户空间请求操作系统内核的服务,EL0 -> EL1
  • 虚拟化权限请求(Hypervisor Call, HVC):操作系统内核请求虚拟化监视器服务,EL1->EL2
  • 安全监视器权限请求(Secure Monitor Call,SMC):非安全世界请求进入安全世界的服务,Normal world -> secure world

image-20220413092241821

有几个需要注意的点:

  • 通常情况下,如果在EL0进行请求,那么异常会发生在EL1,有种特殊情况,就是HCR_EL2.TGE被置位的时候,这个时候异常会发生在EL2。
  • 异常和中断类似都有个异常向量表。程序发生异常的时候,CPU会会跳转到到更高层级的handler里面,查找异常向量表,找到本级异常的handler的地址,再跳转过去。这里先留个疑问,为什么要找到lower level的handler?

2.2 中断嵌套

我们要注意一个比较重要的概念,中断嵌套(中断抢占),从定义上来看4

中断嵌套指中断系统正在执行一个中断服务L时,有另一个优先级更高的中断H触发,这时中断系统会暂时中止当前正在执行低优先级中断服务程序L,而去处理高级别中断H,待处理完毕,再返回被中断了的中断服务程序L继续执行。

从处理器的角度来看,ARM64是支持中断嵌套的,我们可以从三个角度来看5

  • ARM Core支持中断嵌套吗?支持!
  • GIC中断控制器支持中断嵌套吗?支持!
  • Linux Kernel有没有中断嵌套的使用呢?不支持
  • RTOS kernel哟没有中断嵌套的使用?支持!

2.2.1 ARM Core的中断嵌套

首先来看ARM CORE支持中断嵌套吗?答案 支持! 但是是有一个前提,在进入中断处理时,PSTATE的I、F、A等比特位是MASK的,软件中需要主动unmask后,那么就可以中断嵌套了。如下图所示,正是ARM Core支持中断嵌套的一个示例(或者叫模型):

image-20220413092241821

每次调用要保存中断的上下文。

2.2.2 GIC中断控制器中断嵌套

继续看GIC中断控制器支持中断嵌套吗?答案 支持中断抢占,支持中断嵌套。介绍以下优先级和抢占的概念:

  • 每个 INTID(中断号) 都有一个优先级(用寄存器GICD_IPRIORITYnGICR_IPRIORITYn 表示),它是一个 8 位无符号值。 0x00 是可能的最高优先级,0xFF 是可能的最低优先级。
  • 每个 PE 在其 CPU interface中都有一个优先级掩码寄存器 (ICC_PMR_EL1)。 该寄存器设置将中断转发到该 PE 所需的最低优先级。 只有优先级高于寄存器值的中断才会发送给 PE
  • GICv3具有运行优先级的概念。 当 PE 响应中断时,它的运行优先级变为该中断的优先级。 当 PE 写入 EOI 寄存器之一时,运行优先级将返回其先前值。 如果 PE 尚未处理中断,则运行优先级为空闲优先级 (0xFF)。 只有优先级高于运行优先级的中断才能抢占当前中断。

GIC中断控制器的中断嵌套是可以配置的。分为Without preemption和With preemption两种模式:

  • Without preemption的情况下,高优先级的中断无法抢占正在active的中断,只能等active的中断执行完了、返回了,高优先级的中断才能发生"抢占"(这里还说抢占,合适吗? 此种情况应属于抢占pendding中断);
  • With preemption,描述的其实就是:一个中断正在执行,然后另一个更高优先级的中断打断了它, 这也就是正是我们所说的中断嵌套。
那么对于一个gicv3的IP,优先级肯定是有的,它到底是Without preemption 还是With preemption呢? 如何配置的呢?

请查略ICC_BPRn_EL1寄存器,该寄存器定义优先级值字段分成两部分的点,即组优先级字段和子优先级字段。 组优先级字段确定组 1 中断抢占。 换句白话来解释就是,中断优先级被分成了两部分,如下图所示,ICC_BPRn_EL1寄存器的BIT[2:0]定义了下图中的N的值6

Set priority mask and binary point registers. The CPU interface contains the Priority Mask register (ICC_PMR_EL1) and the Binary Point registers (ICC_BPRn_EL1). The Priority Mask sets the minimum priority an interrupt must have in order to be forwarded to the PE. The Binary Point register is used for priority grouping and preemption. The use of both of these registers is described in more detail in Chapter 5.

image-20220413092241821

2.2.3 Linux Kernel and RTOS

Linux Kernel

继续看Linux Kernel操作系统有没有使用中断嵌套?答案 没有使用!

首先查看ICC_BPRn_EL1寄存器的配置:

https://elixir.bootlin.com/linux/v5.13.19/source/drivers/irqchip/irq-gic-v3.c#L1005

image-20220813093047904

写入的是0,也就是意味着,N=0, 即下图的第一行,也就是说抢占是开启的。

image-20220413092241821

继续看,针对每一个 INTID(中断号) 的priority的配置,如下所示,在gic初始化阶段,给每一个 INTID(中断号) 都配置成了一样的优先级,值位0XA5。 也就是所有中断的优先级都是一样的。

https://elixir.bootlin.com/linux/v5.13.19/source/drivers/irqchip/irq-gic-v3.c#L803

image-20220813093913483

事实上,在gicv3代码中,提供了一个接口,可以单独针对某一中断设置优先级。查略该函数用途,仅仅是为NMI中断设置的(注:Linux Kernel中armv8体系目前还没有该中断,ARMV9新增了一类NMI中断),其值为0Xa5 & 0x7f = 0x25,该值小于0XA0,所有该优先级大于其它中断的优先级。

https://elixir.bootlin.com/linux/v5.13.19/source/drivers/irqchip/irq-gic-v3.c#L460

image-20220813094025953

##### RTOS的中断抢占(ARM Cortex-CM3)478

目前比较流行的几种嵌入式实时操作系统有uC/OS、RT-Thread、FreeRTOS等,对外都宣称它们是嵌入式实时操作系统,那么什么叫实时呢?所谓【实时】 其实就是【及时】,能够及时的处理各种任务和中断,而如何实现【实时/及时】呢,本质上就是要支持:高优先级任务/中断能够抢占低优先级任务/中断,这里的【抢占】本质上就是【中断嵌套】。

image-20220413092241821

关于RTOS系统如何驱动CPU进行中断嵌套,参考78。内部使用了就绪链表的结构体,这部分暂时不在ARMv8中展开讨论。

Exception handling registers9

我们在做加减乘除的指令的时候从PSTATE寄存器的高4位读取NZCV的值,来确定状态。今天要涉及的是PSTATEDAIF的值,这部分是异常处理标志位。根据手册的翻译,如果异常发生,PSTATE信息会被存入到Saved program status register(SPSR_ELn),只有3个,SPSR_EL1, SPSR_EL2, SPSR_EL3。

image-20220413094212731

DAIF就是exception bit mask bits,还有SPSel

  • D: debug exceptions mask
  • A: SError interrupt process state mask,例如异步external abort
  • I:IRQ中断就处理状态掩码
  • F:FIQ中断处理状态掩码

当异常发生的时候,PSTATE寄存器的值(current EL, DAIF, NZCV etc)都会被复制到SPSR_ELn,返回的地址会被也会被存储到ELR_ELn中。

image-20220413102033518

有几点需要注意:

  • SPSR_EL1,2,3是三个不同的寄存器实体

  • 在同步或者SError异常,ESR_ELn也会被更新,用于表明产生异常的原因。

  • 我们已经看见SPSR已经为异常的返回记录了必要的状态信息,还要知道link registers用于存储程序地址信息。ARM64提供一个独立的链接寄存器用于函数调用和异常返回

  • 我们在做汇编跳转的实验的时候需要备份X30寄存器的地址,在异常返回的时候它的值也是会被指令地址更新。X30子函数的返回地址,ret指令返回;ELR_ELx存储异常返回地址,使用ERET指令**。

  • ELR_ELn用于存储从一个异常返回到程序的地址。对于一些异常,这个地址是产生异常的下一条指令地址,但有些情况,例如SVC指令的执行,因为发生了异常,我们需要回来的时候继续执行原指令而不是下一条指令,这个就有说法了。异步异常,ELR_ELn指向一个还没有被执行的第一条指令的地址,这个时候我们在handler的代码里面是可以修改ELR_ELn指向的地址的,比如发生了abort指令的异常,异常结束之后我们还需要回到发生异常的那条指令上去。所以就必须要对地址进行减4或者减8。

  • SPSR和ELR寄存器,每一个异常等级都有自己的栈寄存器SP_ELn。这些寄存器用于指示不同EL层级专用的栈。如果没有专用的栈指针,当函数执行到异常处理的handler的时候,SP指针就会被异常处理函数覆盖,所以每一级的SP_EL都会存储自己的原始的地址。

  • handler可能会把SP_ELn切换到SP_EL0。比如,SP_EL1可能指向一块由内核进行安全维护的比较小的内存区域,SP_EL0指向一块内核没有负责安全维护的比较大的内核任务栈空间(有可能发生溢出),这个时候可能EL1溢出到EL0。这个时候需要写[SPSel]位,来进行SP_EL的切换:

    msr SPSel, #0   // switch to SP_EL0
    msr SPSel, #1   // switch to SP_EL1

4. Syn and Asyn exceptions

异步和同步异常在ARM64处理器里面处理的方式不太一样,而且使用的寄存器还有寄存器的行为都是不一样的。这一小节笔记主要就是来分析和记录一下同步异步处理的不同点,后面还有关于aarch32和aarch64混合的时候,arm64的一些行为。ARM的手册在最开始的时候就强调一个事情,return address是否包含具体的发生异常的原因。这个有什么用不太清楚,手册里面注明,对于同步的异常,回退地址总能包含发生异常的原因,然而对于异步的异常,回退地址可能会包含发生异常的原因。

异步的异常主要有三种:

  • IRQ中断
  • FIQ快速中断
  • SError系统错误,(最可能是异步数据错误Data Abort,从cache回写脏数据到外存)

同步的异常包含:

  • MMU的指令错误(执行标记为不可执行的内存)
  • MMU的数据错误(读取权限问题,对齐问题)
  • SP/PC对齐检查错误
  • 同步外部错误,读translation table
  • 无法识别的指令
  • 调试异常。

4.1 sync aborts

同步异常的触发有很多种情况,这里需要注意的是,异常不代表错误,异常状态也可能是系统正常处理的一部分,比如,在Linux里面内存缺页错误,还有我们之前说预计加载指令的page fault10,这都是为了提升性能必须要制造的异常场景。同步的异常处理包含:

  • Abort form the MMU
  • SP PC alignment checking
  • Unallocated instructions
    • opcode缺失
    • 权限不匹配
    • 执行被禁止的指令
    • PSTATE.IL被置位的时候
  • Service Calls(SVCs, SMCs, HVCs)

在ARMv7架构里面,预取指令prefetch,数据错误Data abort,未定义异常undef是分开的指令。而在AARCH64架构里面,都视为同步异常,如果我们想在AARCH64体系架构里面知道具体的异常是什么东西,那就需要配合其他寄存器来获取更全面的信息,ESR_ELn,FAR_ELn。

4.2 Handling synchronous exceptions

  • Exception Syndrome Register (ESR_ELn):异常的具体原因。
  • Fault Address Register (FAR_ELn):提供发生同步指令错误、数据错误和对齐错误的虚拟地址。
  • Exception Link register (ELR_ELn):数据访问发生错误的指令的地址,这个寄存器通常在内存错误之后被写入,还有一种情况也会写入这个地址,就是访问非对齐的地址。

有个AARCH32和AARCH64混合的场景,我们在运行32位程序的时候需要进入到EL1执行,这个时候32位的程序发生了异常,需要进入到EL2处理,此时EL2还是AARCH64的,这个时候我们想要获取到32位程序的异常,需要进入到EL2的FAR寄存器读取信息,EL1的AARCH32的执行状态的FAR,全部清0。

对于比较高等级的EL2(hypervisor),EL3(secure monitor):

  • 同步异常发生在自己的等级(EL2, EL3)或者是更高的等级(EL2)
  • 异步异常能够被路由到更高的等级(EL2, EL3)是被hypervisor或者secure monitor来处理的。可以通过SCR_EL3指定到EL3的路由信息,HCR_EL2来指定到EL2的路由信息。
  • 当然这些寄存器配置也包含IRQ,FIQ,Serror的路由。

4.3 system calls

EL0 call EL1

执行应用程序想要使用特权指令这个时候就需要系统调用。其中的一种方法就是SVC指令。当应用程序call这个SVC的时候,就会产生异常,处理器理所应当的进入到了更高一级的EL。这个时候如果我们想传递一些参数,就通过写寄存器的方式。

EL1 call EL2/3

如果在EL0,是不允许直接call到EL2或者EL3的,必须通过EL0 call到EL1的os kernel,由kernel发送请求到EL2/3。在EL1的os kernel通过HVC指令call到hypervisor,或者os kernel通过SMC指令call到secure monitor的。如果在处理器里面没有EL3,这个时候就会出现新的异常unallocted instruction。注意,我看手册里面是EL1可以call HVC也可以直接call SMC。

EL2 call EL3

EL2可以call EL3通过SMC指令。如果在EL2/3 向后call HVC指令,也会有个同步异常发生在本异常等级。

4.4 Exe state & Exception level

官方手册里面给了一个图表示EL0 如何 call进入EL1的,可以说这个是一个直接关系,EL0是没有任何异常处理机制的。

image-20220413152357009

image-20220413153039923

我们补充一下关于EL1 call进EL2/3的图,对于同步的异常而言,没什么好说的了,就是一级一级的call进去,但是对于异步的异常而言,就有点不一样了,我们EL1需要call EL2或者EL3的时候,需要配置HCR寄存器决定路由信息,同样的EL3也是需要路由信息的配置的。否则会产生新的异常。

还有一个比较特殊的情况,当有一个从AARCH32发生的异常到AARCH64处理,必须做一些特殊的考虑,AARCH64需要访问32位的寄存器了,直接访问肯定会hang死,所以ARM处理做了一些映射处理:

  • R0-R12 : X0-X12
  • SP LR: X13 - X23
  • R8-R12:X24 - X29

HCR_EL2.RW记录了EL1运行在哪个模式, 1代表aarch64, 0是aarch3211

关于返回的时候,也是通过寄存器找返回地址和返回的模式,在SPSR寄存器M[4:0]记录了返回的模式。

image-20220413180417869

4.5 Exception Table

4.5.1 Table

采用基地址+offset的模式,ARMv8有三个表,VBAR_EL1, VBAR_EL2, VBAR_EL3。 高11位有效。

Address Exception type Description
VBAR_ELn + 0x000 Synchronous Current EL with SP0
+ 0x080 IRQ/vIRQ Current EL with SP0
+ 0x100 FIQ/vFIQ Current EL with SP0
+ 0x180 SError/vSError Current EL with SP0
+ 0x200 Synchronous Current EL with SPx
+ 0x280 IRQ/vIRQ Current EL with SPx
+ 0x300 FIQ/vFIQ Current EL with SPx
+ 0x380 SError/vSError Current EL with SPx
+ 0x400 Synchronous Lower EL using AArch64
+ 0x480 IRQ/vIRQ Lower EL using AArch64
+ 0x500 FIQ/vFIQ Lower EL using AArch64
+ 0x580 SError/vSError Lower EL using AArch64
+ 0x600 Synchronous Lower EL using AArch32
+ 0x680 IRQ/vIRQ Lower EL using AArch32
+ 0x700 FIQ/vFIQ Lower EL using AArch32
+ 0x780 SError/vSError Lower EL using AArch32

如果kernel code执行在EL1,IRQ一个中断过来,这时候SP_EL1,而且SPSel bit被置位,执行地址就应该是 VBAR_EL1 + 0x280。

4.5.2 Linux Kernel

以下是linux 4.14内核的entry.S文件中书写的异常向量表。

/*
 * Exception vectors.
 */

	.align	11
ENTRY(vectors)
	ventry	el1_sync_invalid		// Synchronous EL1t
	ventry	el1_irq_invalid			// IRQ EL1t
	ventry	el1_fiq_invalid			// FIQ EL1t
	ventry	el1_error_invalid		// Error EL1t

	ventry	el1_sync			// Synchronous EL1h
	ventry	el1_irq				// IRQ EL1h
	ventry	el1_fiq_invalid			// FIQ EL1h
	ventry	el1_error_invalid		// Error EL1h

	ventry	el0_sync			// Synchronous 64-bit EL0
	ventry	el0_irq				// IRQ 64-bit EL0
	ventry	el0_fiq_invalid			// FIQ 64-bit EL0
	ventry	el0_error_invalid		// Error 64-bit EL0

#ifdef CONFIG_COMPAT
	ventry	el0_sync_compat			// Synchronous 32-bit EL0
	ventry	el0_irq_compat			// IRQ 32-bit EL0
	ventry	el0_fiq_invalid_compat		// FIQ 32-bit EL0
	ventry	el0_error_invalid_compat	// Error 32-bit EL0
#else
	ventry	el0_sync_invalid		// Synchronous 32-bit EL0
	ventry	el0_irq_invalid			// IRQ 32-bit EL0
	ventry	el0_fiq_invalid			// FIQ 32-bit EL0
	ventry	el0_error_invalid		// Error 32-bit EL0
#endif
END(vectors)

看一下Linux内核发生异常的时候如何调用函数的,这里以el1_fiq_invalid为例,这个异常没有被Linux kernel实现。

image-20220414134727432

异常向量表 inv_entry 1 , BAD_FIQ,指定el为1,备份sp到x0,把esr_el1(存储具体异常原因的)被分到x2接着跳转到bad_mode里面。

4.5.3 save context

Linux内核的arch/arm64/include/uapi/asm/ptrace.h 和arch/arm64/include/asm/ptrace.h 里面定义了一下结构体,用于存储异常上下文。

struct user_pt_regs {
	__u64		regs[31];
	__u64		sp;
	__u64		pc;
	__u64		pstate;
};

struct pt_regs {
	union {
		struct user_pt_regs user_regs;
		struct {
			u64 regs[31];
			u64 sp;
			u64 pc;
			u64 pstate;
		};
	};
	u64 orig_x0;
	u64 syscallno;
};

5. Excepiton Entry

5.1 Entry

上面罗列了很多异常元素的概念,现在需要有机的将上面的内容串起来,看一下ARMv8到底如何处理的异常。CPU需要做的事情:

  • S1: PSTATE保存到SPSR_ELx
  • S2: 返回地址保存到ELR_ELx
  • S3: PSTATE寄存器里面的DAIF域都设定为1(等同于关闭调试异常、SError,IRQ FIQ中断)
  • S4: 更新ESR_ELx寄存器(包含了同步异常发生的原因)
  • S5: SP执行SP_ELx
  • S6: 切换到对应的EL,接着跳转到异常向量表执行

操作系统需要做的事情:

  • 识别异常发生的类型
  • 跳转到合适的异常向量表(包含异常跳转函数)
  • 处理异常
  • 操作系统执行eret指令

在操作系统执行ERET指令,CPU需要从ELR_ELx寄存器中恢复PC的指针;从SPSR_ELx寄存器恢复处理器状态。

5. Examples

5.1 switch EL2 to EL1

基于树莓派和QEMU,上电跳转的时候benos处于EL2下,请把benos切换到EL1中运行。

【提示】:

  • 设定HCR_EL2寄存器,最重要的是Bit 31的RW域,表示EL1要在哪里执行
  • 设定SCTLR_EL1寄存器,需要设定大小端和关闭MMU
  • 设定SPSR_EL2寄存器,设定模式M域为EL1h,另外关闭所有的DAIF
  • 设定异常返回寄存器ELR_EL2,让其返回el1_entry汇编的函数里面。
  • 执行ERET

【程序流】:

flowchart TB
   A[start] --> B{1, EL n?} -- EL1 --> C[ERET]
                B -- EL2 --> D[Start switching EL1]
   
   D -- bitRW --> E[2,set HCR_EL2 reg]
   E -- close MMU --> F[3,set SCTLR_EL1 reg]
   F -- close DAIF --> G[4,SPSR_EL2 reg]
   G --> H[5,set ELR_EL2 reg]
   H --> J[6,ERET]
   
Loading

Node 1: judge ELn

要判断当前的是EL几,可以通过访问CurrentEL12系统寄存器来确定,这个是个64位的寄存器。

EL,bit[3:2]

EL Meaning
0b00 EL0.
0b01 EL1.
0b10 EL2.
0b11 EL3.
#define BITS_EL0 (0)
#defien BITS_EL1 (0x1 << 2)
#define BITS_EL2 (0x1 << 3)
#define BITS_EL3 (0x3 << 2)

MRS <Xt>, CurrentEL

msr x5, CurrentELs
cmp x5, #BITS_EL2
b.eq el3_entry
b el2_entry

Node 2: set HCR_EL2 reg (about AArch64)

这个HCR_EL211主要用于设定我们的目标异常等级。

image-20220413201618550

主要是配置这个:

image-20220413201713007

#define HCR_EL2_REG_SET_AARCH64 (1 << 31)

ldr x0, =HCR_EL2_REG_SET_AARCH64
msr hcr_els, x0

Node 3: MMU closing

SCTLR_EL113EL1的系统控制寄存器上面可以关掉MMU。

image-20220413202235337

#define SCTLR_EL1_REG_CLEAR_MMU (0)
ldr x0, =SCTLR_EL1_REG_CLEAR_MMU
msr sctrl_el1, x0

Node 4: Clear DAIF

SPSR_EL2寄存器14

image-20220413202911924

#define SPSR_MASK_ALL (0x7 << 6)
#define SPSR_EL1h (5 << 0)
#define SPSR_EL2h (9 <<0)
#define SPSR_EL1 (SPSR_MASK_ALL | SPSR_EL1h)
#define SPSR_EL2 (SPSR_MASK_ALL | SPSR_EL2h)

ldr x0, =SPSR_EL1
msr spsr_el2, x0

Node 5: Set entry

ELR_EL215设定异常返回地址。

When HCR_EL2.E2H is 1, without explicit synchronization, access from EL2 using the mnemonic ELR_EL2 or ELR_EL1 are not guaranteed to be ordered with respect to accesses using the other mnemonic.
Accesses to this register use the following encodings in the System register encoding space:

MRS <Xt>, ELR_EL2

MSR ELR_EL2, <Xt>

adr x0, el1_entry
msr elr_el2, x0

el1_entry:
	bl print_el

5.2 register exception vectors

// entry.S

/*
| `VBAR_ELn + 0x000` | Synchronous    | Current EL with SP0 |
| `+ 0x080`          | IRQ/vIRQ       | Current EL with SP0 |
| `+ 0x100`          | FIQ/vFIQ       | Current EL with SP0 |
| `+ 0x180`          | SError/vSError | Current EL with SP0 |
 */
#define BAD_SYNC 0
#define BAD_IRQ 1
#define BAD_FIQ 2
#define BAD_ERROR 3

.global user_str0
// .align 3
// user_str0:
//     .string "entry.S: join inv_entry \n"
/*
 * inv_entry el, reason
 */
    .macro	inv_entry el, reason
	//adrp x0, user_str0
	//add x0, x0, :lo12:user_str0
	//bl put_string_uart
    mov x0, SP
    mov x1, #\reason
    mrs x2, ESR_EL1
    b bad_mode
    .endm

/*
 * vector_table entry label
 */
    .macro	vtentry label
    .align 7
    b \label
    .endm

/*
| ------------------ | -------------- | ---------------------- |
| `VBAR_ELn + 0x000` | Synchronous    | Current EL with SP0    |
| `+ 0x080`          | IRQ/vIRQ       | Current EL with SP0    |
| `+ 0x100`          | FIQ/vFIQ       | Current EL with SP0    |
| `+ 0x180`          | SError/vSError | Current EL with SP0    |
| ------------------ | -------------- | ---------------------- |
| `+ 0x200`          | Synchronous    | Current EL with SPx    |
| `+ 0x280`          | IRQ/vIRQ       | Current EL with SPx    |
| `+ 0x300`          | FIQ/vFIQ       | Current EL with SPx    |
| `+ 0x380`          | SError/vSError | Current EL with SPx    |
| ------------------ | -------------- | ---------------------- |
| `+ 0x400`          | Synchronous    | Lower EL using AArch64 |
| `+ 0x480`          | IRQ/vIRQ       | Lower EL using AArch64 |
| `+ 0x500`          | FIQ/vFIQ       | Lower EL using AArch64 |
| `+ 0x580`          | SError/vSError | Lower EL using AArch64 |
| ------------------ | -------------- | ---------------------- |
| `+ 0x600`          | Synchronous    | Lower EL using AArch32 |
| `+ 0x680`          | IRQ/vIRQ       | Lower EL using AArch32 |
| `+ 0x700`          | FIQ/vFIQ       | Lower EL using AArch32 |
| `+ 0x780`          | SError/vSError | Lower EL using AArch32 |
| ------------------ | -------------- | ---------------------- |
 */
.align 11
.global vectors
vectors:
/*
| Current EL with SP0
| ------------------ | -------------- | ---------------------- |
| `VBAR_ELn + 0x000` | Synchronous    | Current EL with SP0    |
| `+ 0x080`          | IRQ/vIRQ       | Current EL with SP0    |
| `+ 0x100`          | FIQ/vFIQ       | Current EL with SP0    |
| `+ 0x180`          | SError/vSError | Current EL with SP0    |
| ------------------ | -------------- | ---------------------- |
 */
    vtentry el1_sync_invalid
    vtentry el1_irq_invalid
    vtentry el1_fiq_invalid
    vtentry el1_error_invalid
 /*
| Current EL with SPx
| ------------------ | -------------- | ---------------------- |
| `+ 0x200`          | Synchronous    | Current EL with SPx    |
| `+ 0x280`          | IRQ/vIRQ       | Current EL with SPx    |
| `+ 0x300`          | FIQ/vFIQ       | Current EL with SPx    |
| `+ 0x380`          | SError/vSError | Current EL with SPx    |
| ------------------ | -------------- | ---------------------- |
  */
    vtentry el1_sync_invalid
    vtentry el1_irq_invalid
    vtentry el1_fiq_invalid
    vtentry el1_error_invalid
/*
| Lower EL using AArch64
| ------------------ | -------------- | ---------------------- |
| `+ 0x400`          | Synchronous    | Lower EL using AArch64 |
| `+ 0x480`          | IRQ/vIRQ       | Lower EL using AArch64 |
| `+ 0x500`          | FIQ/vFIQ       | Lower EL using AArch64 |
| `+ 0x580`          | SError/vSError | Lower EL using AArch64 |
| ------------------ | -------------- | ---------------------- |
 */
    vtentry el0_sync_invalid
    vtentry el0_irq_invalid
    vtentry el0_fiq_invalid
    vtentry el0_error_invalid
/*
| Lower EL using AArch32
| ------------------ | -------------- | ---------------------- |
| `+ 0x600`          | Synchronous    | Lower EL using AArch32 |
| `+ 0x680`          | IRQ/vIRQ       | Lower EL using AArch32 |
| `+ 0x700`          | FIQ/vFIQ       | Lower EL using AArch32 |
| `+ 0x780`          | SError/vSError | Lower EL using AArch32 |
| ------------------ | -------------- | ---------------------- |
 */
    vtentry el0_sync_invalid
    vtentry el0_irq_invalid
    vtentry el0_fiq_invalid
    vtentry el0_error_invalid

el1_sync_invalid:
    inv_entry 1, BAD_SYNC
el1_irq_invalid:
    inv_entry 1, BAD_IRQ
el1_fiq_invalid:
    inv_entry 1, BAD_FIQ
el1_error_invalid:
    inv_entry 1, BAD_ERROR
el0_sync_invalid:
    inv_entry 0, BAD_SYNC
el0_irq_invalid:
    inv_entry 0, BAD_IRQ
el0_fiq_invalid:
    inv_entry 0, BAD_FIQ
el0_error_invalid:
    inv_entry 0, BAD_ERROR

/* make 2 byte bias */
string_test:
    .string "t001"

.global assert_alignment
assert_alignment:
    ldr x0, =0x80002
    ldr x1, [x0]
    ret

需要在boot.S中注册向量表

	ldr x5, =vectors
	msr VBAR_EL1, x5
	isb

kernel最后跳转:

#include "uart.h"

#define  __u64  unsigned long
#define  u64 	__u64

extern void assert_alignment();

struct user_pt_regs {
	__u64		regs[31];
	__u64		sp;
	__u64		pc;
	__u64		pstate;
};

struct pt_regs {
	union {
		struct user_pt_regs user_regs;
		struct {
			u64 regs[31];
			u64 sp;
			u64 pc;
			u64 pstate;
		};
	};
	u64 orig_x0;
	u64 syscallno;
};

#define read_sysreg(reg) ({ \
		unsigned long _val; \
		asm volatile("mrs %0," #reg \
		: "=r"(_val)); \
		_val; \
})

#define write_sysreg(val, reg) ({ \
		unsigned long _val = (unsigned long)val; \
		asm volatile("msr " #reg ", %x0" \
		:: "rZ"(_val)); \
})

static const char * const bad_mode_handler[] = {
	"Sync Abort",
	"IRQ",
	"FIQ",
	"SError"
};

void bad_mode(struct pt_regs *regs, int reason, unsigned int esr)
{
	uart_send_string("failed\n");
#if 0
	printk("Bad mode for %s handler detected, far:0x%x esr:0x%x\n",
			bad_mode_handler[reason], read_sysreg(far_el1),
			esr);
#endif
}
// load_image /Users/carlos/workspace/work/armv8-train/lab12/benos.bin 0x80000
void kernel_main(void)
{
	uart_init();
	//init_printk_done();
	//printk("printk: printk is ready\n");
	uart_send_string("uart: hello kernel main\n");
	assert_alignment();
	uart_send_string("uart: end kernel main.\n");
	while (1) {
		uart_send(uart_recv());
	}
}

Reference and Change Log

[1]: 2022-8-13: 增加 2.3章节,解说中断嵌套机制

Footnotes

  1. ARM Cortex-A Series Programmer's Guide for ARMv8-A - Fundamentals of ARMv8

  2. ARM Cortex-A Series Programmer's Guide for ARMv7-A - ARM Processor Modes and Registers

  3. 【内核教程第六十四讲】Linux内核异常处理 - 16:51

  4. RTOS系列(1):基础知识——中断嵌套 2

  5. [ARM异常]-ARM体系中是否支持中断嵌套

  6. GICv3_Software_Overview_Official_Release_B.pdf

  7. RTOS内功修炼记(一)—— 任务到底应该怎么写 2

  8. RTOS内功修炼记(二)—— 优先级抢占调度到底是怎么回事? 2

  9. ARM Cortex-A Series Programmer's Guide for ARMv8-A - AArch64 Exception Handling

  10. 04_ELF文件_加载进程虚拟地址空间

  11. Arm Cortex‑A78AE Core Technical Reference Manual - HCR_EL2, Hypervisor Configuration Register, EL2 2

  12. Arm A-profile Architecture Registers - CurrentEL, Current Exception Level

  13. Arm Armv8-A Architecture Registers - SCTLR_EL1, System Control Register (EL1)

  14. Arm Armv8-A Architecture Registers - SPSR_EL2, Saved Program Status Register (EL2)

  15. Arm A-profile Architecture Registers - ELR_EL2, Exception Link Register (EL2)

OMAPL138制作SD卡启动盘及重装Linux系统

OMAPL138制作SD卡启动盘及重装Linux系统

手里的创龙的OMAPL138平台的系统SSH坏掉了,我重新移植了openssh还是不好使,没有办法了只能重装OMAPL138的系统了,按照创龙给的文档《SD卡启动盘制作》按照步骤进行,当执行一个叫做mksdboot-tl.sh文件时候出现以下报错:(倒数第4行)

sfdisk 无效选项 -- D,然后我进入到了这个脚本文件中查看了这一步骤执行了什么命令,发现:

sfdisk -D -H ....我查看了sfdisk的手册,并没有发现-D参数,**咨询了创龙的AE,他们给的说法是,需要换ubuntu 12.04系统,而我用的是ubuntu 16.04。我没有去换这个系统,我觉得也不应该去换,我们应该去寻找问题本质,从根源理论上出发,不能让问题被动地去改变我们,而我们应该去主动解决问题!!**带着好奇心,一步一步的学习,经过一天一夜的锤炼,终于把这个问题解决了,成功的把系统写入了新的SD卡,当看见一个崭新的系统在我面前的时候,我真的挺激动的。这也正是技术有趣的地方。

废话不多说了,进入正题。


【导语】:OMAPL138是德州仪器的ARM+DSP架构,实际上ARM这块依旧按照TI Sitara系列ARM的套路来的,基本上所有的板子都可以大同小异的按照这个本文提供的方法进行SD卡启动盘的制作。在本博客中将要分享SD卡分区重新装在嵌入式Linux在OMAPL138或(TI其他ARM的平台)

本机环境和准备工作

  • 本机工作于Ubuntu 16.04.3 amd64 系统
  • u-boot-tools组件 (使用 sudo apt-get install u-boot-tools命令安装)
  • fdisk命令,dd命令,mkfs命令等
  • OMAPL138的rootfs根文件系统及boot文件(已打好包上传到百度云见本文附件)

SD卡的格式化

OMAPL138的Linux系统和引导都在SD里面,所以SD卡的分区十分重要,在这里要区分SD的分区和SDka分区的类型和名称。OMAPL138的文档中给出要分为两个区一个boot(2G大小,格式:b型 FAT32)和一个rootfs(不少于4G,85型 Linux文件系统)

1. SD卡分区

插上我们的USB读卡器,等待系统读取了SD卡的信息。输入:

  • 查看分区

    sudo fdisk -l

    得到下面截图的图片,这里有两个注意点,**fdisk命令十分危险,一定要看清楚后执行,博主曾经的时候因为输错一个字母导致整个机械硬盘格式化,花费了将近一天恢复。**重点在看清楚/dev/sdc这个sdc,(插入不同的USB口Linux分配的挂在节点名称就不一样,可能是sdd,可能是sde,总之看清楚确认好是你的目标内存卡

    然后看倒数两行的信息,现在这个SD卡有两个分区,分别是sdc1和sdc2,我们现在要把这两个分区全部清除掉(你的可能有三四个分区,也可能只有一个分区

  • fdisk操作

    上面的操作已经获得了/dev/sdc的设备节点名称,然后我们对/devsdc进行分区,需要建立两个分区,一个boot(FAT32型,大约2G大小)和rootfs区(Linux文件系统型,内存卡剩余大小全给它)

    解挂SD卡:

    sudo umount /dev/sdc*

    这里加入通配符*就是解挂所有分区。

    sudo fdisk /dev/sdc

    会有一个这样的和用户交互的界面,可以输入p,回车,查看当前/dev/sdc分区状态。

    然后我们删除所有分区,按照图片输入:

    到此为止我们删除了所有的分区,然后我们就开始建立分区。

    建立分区:


    到此为止,第一个分区我们的boot分区建立完毕。

    建立第二个分区rootfs分区:

    从图中可以看出我们已经建立了一个linux类型的12.9GiB大小的分区,这个就是rootfs分区,不需要修改文件类型。

    通过p我们查看建立的分区,就是我们刚才建立的分区,重点3个位置,启动那个*要在sdc1位置,两个文件类型一个是FAT32 一个是Linux即可。

    还有最后一步,非常重要,输入w 对结果进行保存。

    然后,fdisk会自动退出。输入

    sudo fdisk -l命令查看是否建立成功。

    如果的确是这样的输出,就成功了。

  • 格式化分区

    我们进行了分区之后就开始对分区进行格式化操作,并通过命令指定分区卷名字。

    • 把读卡器拔了重新插一次。

    • 解挂分区: sudo umount /dev/sdc

    • 对/dev/sdc1进行格式化: sudo mkfs.vfat -F 32 -n "boot" /dev/sdc1

    • 对/dev/sdc2进行格式化:sudo mke2fs -j -L "rootfs" /dev/sdc2

      这一步骤会提示“无论如何都要继续(y,n)” 输入 y

      提示“Writing superblocks and filesystem accounting information:” 直接回车

    到此,我们完成了格式化。

建立boot引导区

内存卡的工作已经做完了,我们开始进行boot引导区的建立,在本文的文件附录下载压缩包mksdboot.rar文件,然后解压到你linux你任意的位置,你喜欢哪里就哪里,我解压到~/script/mksdboot目录下了,以下所有命令都以这个路径为例,自己注意修改。

boot.cmd文件

文件里面写入是这些内容

mmc rescan 0
setenv bootargs ${mem_args} console=ttyS2,115200n8 root=/dev/mmcblk0p2 rw ip=off eth=${ethaddr} rootwait

使用mkimage命令建立boot.scr文件

mkimage -A arm -O linux -T script -C none -a 0 -e 0 -n 'Execute uImage' -d ~/script/mksdboot/boot.cmd ~/script/mksdboot/boot.scr

然后我们进入到这个目录查看会生成boot.scr文件,这个文件就是我们要写入到boot区域的文件。

准备boot文件

  1. 把读卡器拔了,然后再重新插一次

  2. 把boot目录里面的所有文件都拷贝到内存卡的boot分区。sudo cp ~/script/mksdboot/boot/* /media/delvis/boot/

3) 解挂/dev/sdc: sudo umount /dev/sdc*

4) 重点:使用dd命令烧写u-boot.ais文件sudo dd if=/home/delvis/script/mksdboot/boot/u-boot.ais of=/dev/sdc seek=10

如图,表示成功。(这里请使用绝对完全的路径,不要使用终端里面例如 ~ 这个符号代表的主目录)

建立Linux的文件系统

1 解压文件系统到rootfs

这个就没有什么了,**把内存卡拔了再插一下,**在压缩包的filesystem里面有rootfs.tar.zb2文件,使用解压命令,解压到内存卡的rootfs区域即可。

sudo tar -xvf ~/script/mksdboot/filesystem/rootfs.tar.bz2 -C /media/delvis/rootfs

等待解压完成之后,一个启动盘就做好了。

2 建立matrix-gui-2.0的连接文件

通过运行这个脚本来进行:注意修改脚本里面的路径,在5和6行,这个脚本路径是我自己SD卡挂在的路径

#!/bin/bash
# check if we need to create symbolic link for matrix 
cd /media/delvis/rootfs/etc/init.d

echo -n "Creating matrix-gui-e symbolic link..."
if [ -f /media/delvis/rootfs/etc/init.d/matrix-gui-e ]; then
  if [ -h /media/delvis/rootfs/etc/rc3.d/*matrix* ]; then
    echo " (skipped) "
  else
    ln -s  ../init.d/matrix-gui-e /tmp/sdk/$$/etc/rc3.d/S99matrix-gui-e
    ln -s  ../init.d/matrix-gui-e /tmp/sdk/$$/etc/rc5.d/S99matrix-gui-e
    echo "Create done"
  fi
fi

sync

3 内存卡启动

可以把内存卡拿下来,放在板子里启动了。注意调节拨码开关让OMAPL138从内存卡启动。

结束语

今天完成了Linux内存卡的制作,完成了,看到了启动界面。


附录文件:

文件:mksdboot.rar

链接:https://pan.baidu.com/s/1pLzc0I3 密码:f9v8


参考文献:

[1] kooking著 SD卡给am335x用作启动介质. TI技术支持社区.

[2] 杰瑞26著. 图解Linux命令--mkfs命令. CSDN博客.

[3] 创龙公司著. mksdboot-tl.sh脚本文件和相关文档. 创龙手册.


版权声明:

1. 本文为MULTIBEANS团队研发跟随文章,未经允许不得转载。

2· 文中涉及的内容若有侵权行为,请与本人联系,本人会及时删除。

3· 尊重成果,本文将用的参考文献全部给出,向无私的工程师,爱好者致敬。

OMAPL多核异构通信驱动AD9833-Notify组件demo

#OMAPL多核异构通信驱动AD9833-Notify组件demo

OMAPL多核通信有三个主要机制,Notify,MessageQ,RegionShare;这里主要利用了Notify机制进行通信控制。

要做一个什么实验?

简单的说,ARM跑一个界面上面有一些按钮,DSP负责驱动AD9833产生正弦、方波和三角波,写入频率信息。这个实验结构是一个经典的单向的传输结构,由用户触发ARM跑的界面上的按钮,发出消息通知DSP,DSP控制AD9833产生波形,写入频率字等信息。

那么ARM的Linux端首选Qt,DSP端的程序使用SYSLINK/BIOS实施操作系统,IPC通讯组件使用Notify。

视频预览:

<iframe height=498 width=510 src='http://player.youku.com/embed/XMzY1MjUwNDI0OA==' frameborder=0 'allowfullscreen'></iframe>

多核通信工程目录结构


几个文件,arm,dsp,run,shared,还有makefile文件,makefile文件自己要会修改。

DSP端程序

DSP端程序对于用户来讲ad9833_dev.c ad9833_server.c main.c 三个主要的文件,

  • ad9833_dev.c 为AD9833底层驱动,负责写时序,写参数的
  • ad9833_server.c 相当于以太网scoket通信因子,负责进行多核通信和调用dev中的api的
  • main.c 为dspbios启动,初始化操作。

环境搭建正确之后,最核心的就是这三个东西,对还有个makefile要配置正确。我在环境调试的时间花的比开发时间多的多,最重要的就是要环境配置正确,库啊,路径啊,这类的。

AD9833底层驱动-ad9833_dev.c

我们这里给出接口函数目录,具体实现不给出:

enum ad9833_wavetype_t{
    SIN,SQU,TRI
};

struct ad9833_hw_t {

    uint16 clk;
    uint16 sdi;
    uint16 fsy;
};
// AD9833结构体表述
typedef struct ad9833_t {

    struct ad9833_hw_t hw;
    struct ad9833_t *self;
    enum ad9833_wavetype_t wave_type;

    u16 delay;

    void (*write_reg)( struct ad9833_t *self, u16 reg_value);
    void (*init_device)( struct ad9833_t *self );
    void (*set_wave_freq)( struct ad9833_t *self , float freqs_data);
    void (*set_wave_type)( struct ad9833_t *self, enum ad9833_wavetype_t wave_type );
    void (*set_wave_phase)( struct ad9833_t *self, u16 phase );
    void (*set_wave_para)( struct ad9833_t *self, u32 freqs_data, u16 phase, enum ad9833_wavetype_t wave_type );
} AD9833;
// 函数列表
void    ad9833_set_para( struct ad9833_t *self,u32 freqs_data, u16 phase, enum ad9833_wavetype_t wave_type );
void    ad9833_device_init( struct ad9833_t *self );
void    ad9833_write_reg( struct ad9833_t *self, uint16_t data );
void    ad9833_delay( struct ad9833_t *self );
void    ad9833_gpio_init( void );
void    ad9833_set_wave_type( struct ad9833_t *self, enum ad9833_wavetype_t wave_type );

void    ad9833_set_phase( struct ad9833_t *self, uint16_t  phase );
void    ad9833_set_freq( struct ad9833_t *self,  float freq );
void    ad9833_dev_destroy( AD9833 *dev );
void	ad9833_dev_new();

AD9833的驱动,按照手册进行编辑,然后封装成这个样子,这里一定需要有的函数是:

  • ad9833_dev_new()
  • ad9833_dev_destroy()

这两个函数需要在ad9833_server里面运行。
AD9833这块就不多说了,我们主要来说多核通信这块的知识。

IPC之Notify机制-ad9833_server.c

结构体建立

ad9833_server结构体的建立:

typedef struct ad9833_server_t {
	// 3个id
    uint8_t host_id;		
    uint8_t line_id;
    uint8_t event_id;
	// 连接状态
    bool connected;
    bool quit;
	// 信号量的机制
    Semaphore_Struct sem_obj;
    Semaphore_Handle sem;
    uint32_t payload;
	// 底层设备,ad9833_dev.c的驱动结构体
    AD9833 *dev;
} AD9833_SERVER ;

*** 3个ID**
host id: 在BIOS里面有设定
line_id,event_id: 在shared文件夹内有个SystemCfg.h里面定义了这两个ID

/* ti.ipc.Notify system configuration */
#define SystemCfg_LineId        0
#define SystemCfg_EventId       7

*** 信号量**

l提供对共享资源的的互斥访问,最多直接64个独立的信号量,信号量请求方式
——直接方式
——间接方式
——混合方式
l不分大小端
l信号量的原子操作
l锁存模式(信号量被使用时)
l排队等待信号量
l获取信号量时产生中断
l支持信号量状态检测
l错误检测和错误中断

通过以上阅读就可以知道信号量是做什么的了。

*** 底层设备**
需要通过server结构体的实例化对AD9833实行操控。

####服务函数

Notify必不可少的几个函数:

  • 事件注册函数:static void ad9833_server_on_event(**uint16_t proc_id, uint16_t line_id, uint32_t event_id, UArg arg, uint32_t payloa**)
  • 事件销毁函数:void ad9833_server_destroy(AD9833_SERVER *server)
  • 运行函数: void ad9833_server_run(AD9833_SERVER *server)
  • 命令接收函数: static uint32_t ad9833_server_wait_command(AD9833_SERVER *server)
  • 命令执行函数:static void ad9833_server_handle_command(AD9833_SERVER *server, uint32_t cmd)

基本上有了这些函数之后,就可以完成对于Notify服务函数的处理:

Ad9833Server	*ad9833_server_new( uint16_t host_id, uint16_t line_id, uint32_t event_id )
{
	Ad9833Server	*server = ( Ad9833Server * )calloc(1,sizeof( Ad9833Server ));
	server->host_id			=	host_id;
	server->line_id			=	line_id;
	server->event_id			=	event_id;
	server->quit			=	false;
	server->connected		=	false;

	server->dev				=	ad9833_dev_new();
	Semaphore_Params	params;
	Semaphore_Params_init( &params );
	params.mode				=	Semaphore_Mode_COUNTING;
	Semaphore_construct(&server->sem_obj,0,&params);
	server->sem = Semaphore_handle(&server->sem_obj);

	if( Notify_registerEvent( \
		server->host_id, \
		server->line_id, \
		server->event_id, \
		ad9833_server_start_event, \
		(UArg)server ) < 0 ) {
		printf( "fail to register event in %d:%d(line:event)", server->line_id, server->event_id );
	}

	return server;
}

static void	ad9833_server_start_event( uint16_t proc_id, uint16_t line_id, uint32_t event_id, UArg arg, uint32_t payload )
{
	Ad9833Server	*server 	=	(Ad9833Server *)arg;

	Notify_disableEvent( server->host_id, server->line_id, server->event_id );
	//ASSERT( server->payload == APP_CMD_NULL );
	server->payload	=	payload;
	Semaphore_post( server->sem );
}


void	ad9833_server_destroy( Ad9833Server *self )
{
	if( !self ) return;
	Notify_unregisterEvent( self->host_id, self->line_id, self->event_id, ad9833_server_start_event, (UArg)self );
	Semaphore_destruct(&self->sem_obj);
	ad9833_dev_destroy(self->dev);
	free(self);

}


void	ad9833_server_run( Ad9833Server *self )
{
	//ASSERT(self);
	printf( "ad9833_server running...\n" );
	while( ! self->quit ){
		uint32_t cmd	=	ad9833_server_wait_command( self );
		ad9833_server_handle_command( self, cmd );
	}

	printf( "ad9833 server is stopped!\n" );

}


static uint32_t	ad9833_server_wait_command( Ad9833Server *self )
{
	Semaphore_pend( self->sem, BIOS_WAIT_FOREVER );
	uint32_t cmd = self->payload;
	self->payload	=	APP_CMD_NULL;
	Notify_enableEvent( self->host_id, self->line_id, self->event_id );

	return cmd;
}
static uint32_t	ad9833_server_wait_command( Ad9833Server *self )
{
	Semaphore_pend( self->sem, BIOS_WAIT_FOREVER );
	uint32_t cmd = self->payload;
	self->payload	=	APP_CMD_NULL;
	Notify_enableEvent( self->host_id, self->line_id, self->event_id );

	return cmd;
}


static void ad9833_server_handle_command( Ad9833Server *self, uint32_t cmd )
{
	if( !self->connected && cmd != APP_CMD_CONNECTED ) {
		printf( "disconnect client \n" );
	}
	switch( cmd ) {

	case APP_CMD_CONNECTED:
        //ASSERT(! self->connected);
        //LOG_DEBUG("led client had connected");
        self->connected = true;
		break;

	case APP_CMD_DISCONNECTED:
		//ASSERT( self->connected );

		self->connected	=	false;
		self->quit	= true;

		break;

	case	APP_CMD_SETSINE:

		self->dev->set_wave_type( self->dev, SIN );

		break;

	case	APP_CMD_SETSEQ:

		self->dev->set_wave_type( self->dev, SQU );

		break;

	case	APP_CMD_SETTRI:

		self->dev->set_wave_type( self->dev, TRI );

		break;

	case 	APP_CMD_SETFREQ_UP:

		self->dev->set_wave_freq( self->dev, current_freq += 10 );
		if( current_freq > 50000 ) {
			current_freq = 50000;
		}
		break;

	case 	APP_CMD_SETPHASE_UP:

		self->dev->set_wave_phase( self->dev, current_phase += 1 );
		if( current_phase > 360 ) {
			current_phase = 360;
		}

		break;

	case	APP_CMD_SETFREQ_DOWN:

		self->dev->set_wave_freq( self->dev, current_freq -= 10 );
		if( current_freq < 10 ) {
			current_freq = 10;
		}

		break;
	case 	APP_CMD_SETPHASE_DOWN:

		self->dev->set_wave_phase( self->dev, current_phase -= 1 );
		if( current_phase < 1 ) {
			current_phase = 0;
		}
	}
}

SYSBIOS启动服务

AD9833	*ad9833_handle;

Int main()
{ 
    Task_Handle task;
    Error_Block eb;
    System_printf("enter main()\n");
    Error_init(&eb);
    ad9833_handle 	=	ad9833_dev_new();
    task = Task_create(taskFxn, NULL, &eb);
    if (task == NULL) {
        System_printf("Task_create() failed!\n");
        BIOS_exit(0);
    }

    BIOS_start();    /* does not return */
    return(0);
}

Void taskFxn(UArg a0, UArg a1)
{
    System_printf("enter taskFxn()\n");
    printf("Hello sysbios.\n");
    ad9833_handle->set_wave_para( ad9833_handle, 5000, 0, SIN );


    Task_sleep(1500);
    ad9833_handle->set_wave_type( ad9833_handle, SQU );
    Task_sleep(1500);
    ad9833_handle->set_wave_type( ad9833_handle, TRI );
    Task_sleep(1500);
    ad9833_handle->set_wave_freq( ad9833_handle, 1000.0f );
    System_printf("exit taskFxn()\n");
}

到此我们就完成了对于多核通信的Notify DSP端程序。

ARM端Qt程序

在ARM端有Qt程序,Qt主程序中对syslink的初始化,需要注册几个事件:

    SysLink_setup();
    this->m_slave_id    =   MultiProc_getId("DSP");

    if( Ipc_control(this->m_slave_id, Ipc_CONTROLCMD_LOADCALLBACK, NULL ) < 0) {
        LOG_ERROR("load callback failed");
    }

    if( Ipc_control(this->m_slave_id, Ipc_CONTROLCMD_STARTCALLBACK, NULL ) < 0 ) {
        LOG_ERROR("start callback failed");
    }

    m_dev   =   new ad9833_client( this->m_slave_id, SystemCfg_LineId, SystemCfg_EventId );
    if( ! this->m_dev->connect() ) {
        LOG_ERROR("failed to connect to led server");
    }else {
        LOG_DEBUG("connect to led server");
    }

需要建立服务函数:

#include "ad9833_client.h"
#include "ti/syslink/Std.h"
#include "ti/ipc/Notify.h"
#include "unistd.h"
#include "log.h"

ad9833_client::ad9833_client(uint16_t slave_id, uint16_t line_id, uint16_t event_id )
    : m_slave_id(slave_id),m_line_id(line_id),m_event_id(event_id)
{

}

ad9833_client::~ad9833_client() {

}

bool    ad9833_client::connect() {
    int status;

    do {
        LOG_DEBUG("try to connect!\n");
        status = Notify_sendEvent( this->m_slave_id,  \
                                   this->m_line_id,   \
                                   this->m_event_id,  \
                                   APP_CMD_CONNECTED, \
                                   TRUE );
        if( status != Notify_E_EVTNOTREGISTERED ) {
            usleep(100);
        }
    }while( status == Notify_E_EVTNOTREGISTERED );

    if( status != Notify_S_SUCCESS ) {
        LOG_ERROR("failed to send connect command\n");
        return false;
    }

    LOG_DEBUG("send connected command");
    return true;
}

bool    ad9833_client::send_cmd( uint16_t cmd )
{
    int status = Notify_sendEvent( this->m_slave_id, \
                                   this->m_line_id,  \
                                   this->m_event_id, \
                                   cmd,              \
                                   TRUE);
    if( status < 0 ) {
        LOG_DEBUG("fail to send command: %d", cmd);
        return false;
    }
    LOG_DEBUG("send command: %d", cmd);
    return true;
}

bool    ad9833_client::disconnect()
{
    LOG_DEBUG("disconnect with server");
    return this->send_cmd(APP_CMD_DISCONNECTED);
}

bool    ad9833_client::set_freq_down()
{
    LOG_DEBUG("set freq down with server");
    return this->send_cmd(APP_CMD_SETFREQ_DOWN);
}

bool    ad9833_client::set_freq_up()
{
    LOG_DEBUG("set freq up with server");
    return this->send_cmd(APP_CMD_SETFREQ_UP);
}

bool    ad9833_client::set_phase_down()
{
    LOG_DEBUG("set phase down with server");
    return this->send_cmd(APP_CMD_SETPHASE_DOWN);
}

bool    ad9833_client::set_phase_up()
{
    LOG_DEBUG("set phase up with server");
    return this->send_cmd(APP_CMD_SETPHASE_UP);
}
bool    ad9833_client::set_wave_type(WAVE_TYPE type)
{
    if( type == SIN ) {
        LOG_DEBUG("set wave type is sine");
        this->send_cmd( APP_CMD_SETSINE );
    }else if( type == SQU ) {
        LOG_DEBUG("set wave type is squ");
        this->send_cmd( APP_CMD_SETSEQ );
    }else {
        LOG_DEBUG("set wave type is tri");
        this->send_cmd( APP_CMD_SETTRI );
    }
}


#ifndef AD9833_CLIENT_H
#define AD9833_CLIENT_H
#include "stdint.h"
#include "app_common.h"

typedef enum wave_type_t { SIN=0,SQU,TRI } WAVE_TYPE;

class ad9833_client
{
public:
    explicit ad9833_client( uint16_t  slave_id, uint16_t line_id, uint16_t event_id  );
    ~ad9833_client();

    bool    connect();
    bool    disconnect();


    bool    set_wave_type( WAVE_TYPE type );
    bool    set_freq_up();
    bool    set_freq_down();
    bool    set_phase_up();
    bool    set_phase_down();

private:

    bool    send_cmd( uint16_t cmd );

private:

    uint16_t    m_slave_id;
    uint16_t    m_line_id;
    uint16_t    m_event_id;

};

#endif // AD9833_CLIENT_H

源程序: 链接: https://pan.baidu.com/s/1sxjQaalhhtNcIBGKPlnxmg 密码: ya8g

参考文献

[1] ti/wiki, IPC Users Guide/Notify Module, 19 July 2014, at 13:36
[2] ti/wiki, IPC API-Notify.h File Reference 3.40.00.06, 2015
[3] ti/wiki, SysLink UserGuide/Notify, 24 July 2014, at 09:26.

Linux进程之间的通信-管道(上)

Linux进程之间的通信-管道(上)

  • 标准输入、标准输出和标准错误
  • 进程管道
  • popen
  • pipe调用
  • 父进程和子进程
  • 命名管道:FIFO
  • 客户/服务器架构

1. stdin, stdout, stderr

作为Linux进程之间通信的基础,标准输入、输出和标准错误是必须了解的概念。我们在Linux的userspace层面shell上的机制每天都要和这三个东西打交道。我直接抄文献1了:

  • stdin: Stands for standard input. It takes text as input.
  • stdout: Stands for standard output. The text output of a command is stored in the stdout stream.
  • stderr: Stands for standard error. Whenever a command faces an error, the error message is stored in this stream.

1.1 /dev/stdxxx和stdxxx的区别

基础的使用按照参考文献1,这里需要补充的是,在Linux上/dev/stdin,/dev/stdout/dev/stderr三个文件符号分别链接/proc/self/fd/0, /proc/self/fd/1,/proc/self/fd/2文件描述符(file descriptor),他们并不是stdin,stdout好stderr本身,他们只是一个特殊的文件标识用于指示stdxx的2,Jim通过fopen打开函数获取文件句柄返回值的实验也可以看到,如果打开open("/dev/stdin")这类的文件,并不能获取真正的输入流,而是打开的链接文件本身3。关于/dev/stdxxx特殊文件和stdxxx做了区分,就到这里了。下面对pipe在shell上面的使用做一个整理,方便更好理解pipe通信的内部原理和接口使用。

1.2 Piping and Redirecting

image-20220324211237904

Piping:

  • $ echo "hello world" | grep hello : echo的输出 -> stdout -> stdin -> grep
  • $ echo “hello world” |& cat : echo输出 -> stdout + stderr -> stdin -> grep
  • $ anything |& cat stderr -> stdin -> cat

Redirecting:

  • $ echo “hello world” >> hello.txt 追加新的一行字
  • $ echo "hello world" > hello.txt 清空后复写一行字
  • $ cat < hello.txt :把文件展开,喂给cat作为stdin
$ # pyin.py
$ name = input("Enter name\n")
$ email = input("Enter email\n")
$ print("Your name is %s and email is %s" % (name, email))

hello.txt

$ cat < hello.txt 可以得到结果为:

Enter name
Enter email
Your name is carlos and email is [email protected]

不要被迷惑:

$ cat < hello.txt >output.txt

这个情况就是上面结果被写到了output.txt文件里面了。

$ echo “hello world” 1>output.log 2>debug.log stdout被写入output.log, stderr被重定向到debug.log

感谢Sidratul Muntaha1整了标准输出和标准输入在shell层级的使用,后面将会引出在Linux程序设计中C语言接口中的对pipe的处理,对于pipe进程之间的通信,我们也可以看到存在的局限性,需要其中一个进程运行完得到标准输出之后才能被第二个进程使用,并且从性能角度来考虑,透过stdin, stdout文件符号(需要起一个shell)再加上文件索引,传输吞吐的效率并不会太高

1.2 进程管道(Pipe)

1.2.1 API定义4

#include <stdio.h>

FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);

Parameters:

Params I/O Details
const char *command Input The command argument is a pointer to a null-terminated string containing a shell command line. This command is passed to /bin/sh using the -c flag; interpretation, if any, is performed by the shell.
const char *type Output The type argument is a pointer to a null-terminated string which must contain either the letter 'r' for reading or the letter 'w' for writing. Since glibc 2.9, this argument can additionally include the letter 'e', which causes the close-on-exec flag (FD_CLOEXEC) to be set on the underlying file descriptor; see the description of the O_CLOEXEC flag in open(2) for reasons why this may be useful.

Return:

The return value from popen() is a normal standard I/O stream in all respects save that it must be closed with pclose() rather than fclose(3). Writing to such a stream writes to the standard input of the command; the command's standard output is the same as that of the process that called popen(), unless this is altered by the command itself. Conversely, reading from the stream reads the command's standard output, and the command's standard input is the same as that of the process that called popen().

1.2.2 读取

使用popen,返回一个文件句柄,然后通过fread函数就可以得到另一个进程里面输出到标准输出的返回值。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

#define debug_log printf("%s:%s:%d--",__FILE__, __FUNCTION__, __LINE__);printf

#define P_CMD "./helper_process_long_loop.elf 10"

int main(void)
{
    FILE *read_fp = NULL;
    char buffer[BUFSIZ + 1];
    int chars_read;
    memset(buffer, '\0', sizeof buffer);
    read_fp = popen(P_CMD, "r");
    debug_log("popen start ...\n");
    if (NULL == read_fp) {
        debug_log("popen failed\n");
        return -1;
    }
    // read one time.
#if 1
    //chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);
    if (chars_read > 0) {
        debug_log("Output was:  \n\n %s \n\n", buffer);
    }
#else
    // read multi times.
    do {
        chars_read = fread(buffer, sizeof(char), 32, read_fp);
        buffer[chars_read - 1] = '\0';
        debug_log("Output was:  \n\n %s \n\n", buffer);
    } while (chars_read > 0);
#endif
    pclose(read_fp);
    debug_log("popen end ...\n");
    return 0;
}

1.2.3 写入

int test_pipe_write()
{
    FILE *write_fp = NULL;
    char buffer[BUFSIZ + 1];
    int chars_read;
    sprintf(buffer, "hello my name is Carlos\n");
    write_fp = popen("od -c", "w");
    debug_log("popen start ...\n");
    if (NULL == write_fp) {
        debug_log("popen failed\n");
        return -1;
    }
    chars_read = fwrite(buffer, sizeof(char), strlen(buffer), write_fp);
    debug_log("call pclose, wait ...\n");
    pclose(write_fp);
    debug_log("pclose end ...\n");

    return 0;
}

1.2.4 system与popen

我觉得这个话题还是蛮重要的,我也查了不少的资料,网上有很多人做了system和popen的使用对比,也有人做了system和popen的性能对比。我们研究popen的过程,来确定linux对管道创建的进程调度这是非常重要的。system()函数是一个阻塞式的调用API,里面的过程大体是: fork() + execl() + waitpid()5,如果我们不想要创建的子进程阻塞式影响到我们自己进程的程序,我们也可以单独的使用fork execl及waitpid,或许还可以和各种信号配合。然而,popen创建进程只是使用了fork()并没有waitpid()的操作,从popen的源代码里面也可以看到6,因此,popen是一个非阻塞式的调用,system是一个阻塞式的调用

system和popen的调用控制流也是不一样的,popen创建的进程,虽然不在popen进行阻塞,但是是在fread()函数,或者是pclose()的时候才进行阻塞的。换句话说,如果我们希望从popen创建的子进程的标准输出里面拿到数据,就一定要等子进程执行完毕之后才可以拿到。我也做了相关的实验,得到的结论两种情况:

  • 如果使用fread函数读取子进程的标准输出,那么会在fread阻塞。
  • 如果没有使用fread函数,主进程会等待子进程结束之后,在pclose位置阻塞。

我这里设计一个例子,helper进程,会循环方式sleep 10秒的时间,主进程会调用这个helper进程,主进程最终会等待子进程结束之后完成。

// helper_process_long_loop.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define debug_log printf("%s:%s:%d--",__FILE__, __FUNCTION__, __LINE__);printf

int main(int argc, char *argv[])
{
    int i = atoi(argv[1]); // 10 seconds
    debug_log(">test new sleep process: will sleep %d s\n", i);
    while(i --) {
        debug_log(">sleep at %d s\n", i);
        sleep(1);
    }
    debug_log(">test new sleep process: wakeup done.\n");
    return 0;
}

如果使用了fread()会阻塞在fread()函数

image-20220325115549902

如果去掉了fread()函数,则会在调用pclose之前,等待子进程的调用。

image-20220325115924438

因此,可以将上述过程绘制出来, 调用fread的控制流如图所示:

image-20220325120026930

没有调用fread的控制流如图所示:

image-20220325120120180

我们这里对popen进程创建进行对比,并不是说popen是一个创建进程的方法,popen的确有创建进程的功能,但是我们应该把关注点放在建立管道通信上面

1.2.5 popen与SIGCHLD信号

system()接口一直让人诟病,因为其返回值太多,而且依赖于Linux内核做一个初始条件的初始化,例如,一些防止子进程成为僵尸进程的案例中,有人粗暴的喜欢把 signal(SIGCHLD, SIG_IGN)注册为IGN,这样的话,我们不需要在自己的程序里面对子进程的结束进行处理,而依赖于系统进程周期性的回收,而这对system()的使用是致命的,system的使用依赖于Linux内核将SIGCHLD注册为SIG_DFL,否则在wait_pid的时候将找不到子进程而报错ECHILD7,而且使用system还需要注意SIGINT和SIGQUIT这两个信号。而对于popen对SIGCHLD并没有依赖于Linux内核对信号做一些初始化,但是有种场景也是又waitpid引起的——错误的pclose的错误的error状态8

  • 注册信号signal(SIGCHLD, handler)
  • 主线程popen一个长时间的进程,使其阻塞在pclose()
  • fork一个线程,执行时间比主线程短。

1.2.5.1 注册SIGCHLD->handler

此时,可以看到一个错误,在fork()线程退出后,pclose()的阻塞结束了,换句话说,fork进程的退出结束了pclose对线程的阻塞。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

#define debug_log printf("%s:%s:%d--",__FILE__, __FUNCTION__, __LINE__);printf

#define P_CMD "./helper_process_long_loop.elf "
#define CFG_PARENT_PROCESS_EXEC_TIME  12
#define CFG_CHILD_PROCESS_EXEC_TIME 2

void handler(int sig)
{
    (void)sig;
	pid_t id;
	while((id = waitpid(-1, NULL, WNOHANG)) > 0)
		debug_log("wait child success : %d\n", id);
}

int main(void)
{
    FILE *read_fp = NULL;
    char buffer[BUFSIZ + 1];
    char cmd[BUFSIZ];
    int chars_read;
    pid_t pid = 1;
    int ret = 0;
    memset(buffer, '\0', sizeof buffer);
    sprintf(cmd, "%s%d", P_CMD, CFG_PARENT_PROCESS_EXEC_TIME);
    read_fp = popen(cmd, "r");
    debug_log("popen start: father process %d s...\n", CFG_PARENT_PROCESS_EXEC_TIME);
    if (NULL == read_fp) {
        debug_log("popen failed\n");
        return -1;
    }
    signal(SIGCHLD, handler);
    //signal(SIGCHLD, SIG_IGN);
    pid = fork();
    if (pid != 0) {
        debug_log("parent process: call pclose, wait ...\n");
        ret = pclose(read_fp);
        debug_log("parent process: popen end with ret = %d...\n", ret);
    } else {
        debug_log("child process: sleep %d s !\n ",CFG_CHILD_PROCESS_EXEC_TIME);
        sleep(CFG_CHILD_PROCESS_EXEC_TIME);
        debug_log("child process: wakeup !\n ");
    }
    return 0;
}

image-20220325133041897

从实验结果看,pclose已经拿到了一个错误的返回值。

1.2.5.1 注册SIGCHLD->ignore

此时,可以看到一个错误,在fork()线程退出后,pclose()的阻塞结束了,换句话说,fork进程的退出结束了pclose对线程的阻塞。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

#define debug_log printf("%s:%s:%d--",__FILE__, __FUNCTION__, __LINE__);printf

#define P_CMD "./helper_process_long_loop.elf "
#define CFG_PARENT_PROCESS_EXEC_TIME  12
#define CFG_CHILD_PROCESS_EXEC_TIME 2

void handler(int sig)
{
    (void)sig;
	pid_t id;
	while((id = waitpid(-1, NULL, WNOHANG)) > 0)
		debug_log("wait child success : %d\n", id);
}

int main(void)
{
    FILE *read_fp = NULL;
    char buffer[BUFSIZ + 1];
    char cmd[BUFSIZ];
    int chars_read;
    pid_t pid = 1;
    int ret = 0;
    memset(buffer, '\0', sizeof buffer);
    sprintf(cmd, "%s%d", P_CMD, CFG_PARENT_PROCESS_EXEC_TIME);
    read_fp = popen(cmd, "r");
    debug_log("popen start: father process %d s...\n", CFG_PARENT_PROCESS_EXEC_TIME);
    if (NULL == read_fp) {
        debug_log("popen failed\n");
        return -1;
    }
    //signal(SIGCHLD, handler);
    signal(SIGCHLD, SIG_IGN);
    pid = fork();
    if (pid != 0) {
        debug_log("parent process: call pclose, wait ...\n");
        ret = pclose(read_fp);
        debug_log("parent process: popen end with ret = %d...\n", ret);
    } else {
        debug_log("child process: sleep %d s !\n ",CFG_CHILD_PROCESS_EXEC_TIME);
        sleep(CFG_CHILD_PROCESS_EXEC_TIME);
        debug_log("child process: wakeup !\n ");
    }
    return 0;
}

image-20220325133226534

从实验结果看,pclose还是拿到了一个错误的返回值。

因此,建议popen不要和其他进程同步使用,否则会拿到一个错误的状态。除了状态的错误,运行的时序调度都是正确的。

此外,如果我们没有屏蔽掉SIG_CHLD信号,此时没有调用pclose(),那么就->可能<-会出现僵尸进程

1.2.6 底层pipe调用

上层的popen和pclose内部实际上是调用pipe,我们来看一下pipe的使用,popen对应的文件读写fread,fwrite,而pipe这个层级相应的需要用底层的read和write函数来读写操作。

#include <unistd.h>
int pipe(int pipefd[2]);

#define _GNU_SOURCE             /* See feature_test_macros(7) */
#include <fcntl.h>              /* Definition of O_* constants */
#include <unistd.h>

int pipe2(int pipefd[2], int flags);
/* On Alpha, IA-64, MIPS, SuperH, and SPARC/SPARC64, pipe() has the
   following prototype; see NOTES */
#include <unistd.h>
struct fd_pair {
    long fd[2];
};
struct fd_pair pipe(void);

Parameters:

Params I/O Details
int pipefd[2] Input pipefd[0] refers to the read end of the pipe. pipefd[1] refers to the write end of the pipe. Data written to the write end of the pipe is buffered by the kernel until it is read from the read end of the pipe.

Return:

On success, zero is returned. On error, -1 is returned, errno is set to indicate the error, and pipefd is left unchanged.

On Linux (and other systems), pipe() does not modify pipefd on failure. A requirement standardizing this behavior was added in POSIX.1-2008 TC2. The Linux-specific pipe2() system call likewise does not modify pipefd on failure.

**Example 1 **: 管道建立在同一个进程上面

image-20220325142910498

int test_pipe_rw()
{
    int ret = 0;
    int data_process = 0;
    int file_pipes[2];
    const char some_data[] = "hello world";
    char buffer[BUFSIZ + 1];
    memset(buffer, '\0', sizeof buffer);

    ret = pipe(file_pipes);
    if (ret != 0) {
        debug_log("pipe failed, ret = %d\n", ret);
        goto exit;
    }
		// write some_data to pipe
    data_process = write(file_pipes[1], some_data, strlen(some_data));
    debug_log("Wrote the %d bytes\n", data_process);
  	// read buffer from pipe
    data_process = read(file_pipes[0], buffer, BUFSIZ);
    debug_log("read %d bytes: %s\n", data_process, buffer);
exit:
    return ret;
}

**Example 2 **: 父进程和fork的子进程共用pipe,子进程写,父进程读。

image-20220325143327664

这里要注意的是:

  • 子进程故意延迟了5s的时间再写,会发现父进程会阻塞在read函数。
int test_pipe_rw_fork()
{
    int ret = 0;
    int data_process = 0;
    int file_pipes[2];
    const char some_data[] = "hello world";
    char buffer[BUFSIZ + 1];
    pid_t pid = 0;
    memset(buffer, '\0', sizeof buffer);

    ret = pipe(file_pipes);
    if (ret != 0) {
        debug_log("pipe failed, ret = %d\n", ret);
        goto exit;
    }

    pid = fork();
    if (pid == -1) {
        debug_log("fork failed, ret = %d\n", ret);
        goto exit;
    }
    // child process
    else if (pid == 0) {
        sleep(5);
        data_process = write(file_pipes[1], some_data, strlen(some_data));
        debug_log("child wrote the %d bytes\n", data_process);
    }
    // parent process
    else {
        data_process = read(file_pipes[0], buffer, BUFSIZ);
        debug_log("parent read %d bytes: %s\n", data_process, buffer);
    }

exit:
    return ret;
}

**Example 3 **: 并行进程共用pipe,A进程写,B进程读。

image-20220325144923705

  • fd的顺序,为读写顺序, 这里需要先操作file_description[1],再操作file_descirptin[0]。先操作[1]写入,再去用[0]读,是可以读到东西的。而先写0,从1读,是读不到东西的,这是FIFO结构。
  • 栈是LIFO结构,

A进程:

int test_pipe_rw_exec()
{
    int ret = 0;
    int data_process = 0;
    int file_pipes[2];
    char buffer[BUFSIZ + 1];
    char cmd[BUFSIZ];
    pid_t pid = 0;
    memset(buffer, '\0', sizeof buffer);

    ret = pipe(file_pipes);
    if (ret != 0) {
        debug_log("pipe failed, ret = %d\n", ret);
        goto exit;
    }

    pid = fork();
    if (pid == -1) {
        debug_log("fork failed, ret = %d\n", ret);
        goto exit;
    }
    // child process
    else if (pid == 0) {
        debug_log("run the helper_pipe_write.elf send fd = %d\n", file_pipes[1]);
        cmd[0] = file_pipes[1];
        (void)execl("helper_pipe_write.elf", "helper_pipe_write.elf", cmd, (char *)0);
        debug_log("call the helper_pipe_write.elf wrote the bytes\n");
    }
    // parent process
    else {
        debug_log("go on the main process,.....\n");
        data_process = read(file_pipes[0], buffer, BUFSIZ);
        debug_log("parent read %d bytes: %s\n", data_process, buffer);
    }

exit:
    return ret;
}

B进程:

int main(int argc, char **argv)
{
    int ret = 0;
    int data_process = 0;
    int file_pipes = (int) *(char *)argv[1];
    const char some_data[] = "hello world";
    debug_log("helper_pipe_write.elf start, recv the fd = %d ....\n",file_pipes);
    data_process = write(file_pipes, some_data, strlen(some_data));
    debug_log("helper_pipe_write.elf wrote the %d bytes\n", data_process);
    return 0;
}

image-20220325152246960

Ref

Footnotes

  1. What are stdin, stderr and stdout in Bash, Sidratul Muntaha 2 3

  2. echo or print /dev/stdin /dev/stdout /dev/stderr

  3. What are /dev/stdout and /dev/stdin? What are they useful for?

  4. man3-popen

  5. Popen/system, understand the difference between these two functions and fork.

  6. openBSD: popen.c,v 1.17 2005/08/08 08:05:34 espie Exp

  7. Linux下system()函数引发的错误

  8. Linux的system()和popen()差异

Starting with JLink debugger or QEMU

Starting with JLink debugger or QEMU (ARMv8)

  • Debugger: JLink V11
  • Target Hardware: raspiberry 4b
  • Host: Ubuntu 20.04-amd64

Note, the openocd installed by sudo apt-get install openocd is not work on ubuntu 20.04. The error prompted Error: invalid command name "dap". So you shall compile the openocd by yourself. Please refer to the link https://hackaday.io/page/4991-compiling-openocd-from-source-on-ubuntu-1604. (Though it is targetting for ubuntu 16.04, the 20.04 still follow these build steps.)

1. JLink Brief

We should use the three command terminals to launch the Jlink debugger. They are openocd/telnet/gbd-multiarch separately.

40-pin gpio connect

gpio22-27 Alternative Function Assignments all are ALT4.

connect following gpio pin to jlink pin.

PIN NAME GPIO Function ALT
GPIO22 ARM_TRST
GPIO23 ARM_RTCK
GPIO24 ARM_TDO
GPIO25 ARM_TCK
GPIO26 ARM_TDI
GPIO27 ARM_TMS
3.3v Vref
GPIO09 GND

image-20220305204437337

On the openocd command line, should type the cmd is:

sudo openocd -f jlink.cfg -f raspi4.cfg

On the telnet command line, should type the cmds are:

telnet localhost 4444
> halt
> load_image /home/carlos/work/uncleben/armv8_trainning/lab01/benos.bin 0x80000
> step 0x80000

On the gdb command line, should ytpe the cmds are:

gdb-multiarch --tui benos.elf
> target remote localhost:3333
> b ldr_start
> c
> n
> layout regs

2. QEMU Brief

The QEMU is more simple and more quicker than the JLink debugger env.

On the QEMU side, just type the qemu-system-aarch64 -machine raspi4 -nographic -kernel benos.bin -S -s to launch the QEMU. For the gdb-multiarch side, gdb-multiarch --tui benos.elf

image-20220306124845392

gdb-multiarch --tui benos.elf, the localhost port is 1234.

gdb-multiarch --tui benos.elf
> target remote localhost:1234
> b ldr_start
> c
> n
> layout regs

For the config files, you can get them by https://gist.github.com/carloscn/538d57d36b828e52bf8f88d6362b1528

gdb-multiarch --tui benos.elf
gdb> file benos.elf
gdb> target remote localhost:1234
gdb> b ldr_test // 设定断点
gdb> c
gdb> n
gdb> next //下一步
gdb> info register // 查看所有寄存器
gdb> info x1 x2 x3 // 查看x1/x2/x3寄存器
gdb> x 0x80000 // 读取内存0x80000值 32位
gdb> x/xg 0x80000 // 读取内存0x80000值64位
gdb> layout src
gdb> layout regs
gdb> layout split

Openssl EVP to implement RSA and SM2 en/dec sign/verify

Using the EVP interface in openssl to implement RSA and SM2 encrypt decrypt sign and verify (C lauguage)

0. Abstract

Openssl provides a series of interfaces that name is EVP structure. Using the interfaces, it is pretty convenient to implement these algorithms of asymmetric RSA or SM2 encryption decryption signature and verification. This paper sorted out the usage of OPENSSL EVP C-lauguage interface, and implement the SM2 and RSA encrypt decrypt signature and verify. Combining with the code, this paper introduced the related functions of RSA and SM2 algorithm. Finally, ran the program to test module functions.

1. RSA

1.1 What is RSA.

RSA is a algorithm of asymmetric en/decryption which name comes from the first letter of the three men's name who are Rivest Adi and Shamir. There are two types of public key and private key. Using the RSA algorithm to encrypt plaintext, you need to assign the public key string and plaintext body to RSA algorithm, and RSA algorithm return corresponding ciphertext for you finally. When you need to decrypt the ciphertext, a private key required for decryption. So we need to hide the private key information to avoid being obtained by attacker. But public key information can share to everyone who even include the attacker. In genaral, the public key owner should be a person with whom you are communicating confidentially.

RSA cannot only encrypt and decrypt information, but also have the signature and verified function. When we want to sign the information, we need to assign the private key string and information to be signed to algorithm of RSA signature, and RSA signature function will return the signature string that the data length is about one hundred bytes to us. When we want to verify the signature information, use public key to do it. The verified result is bool value, it's either true or false. The signature and verified function can help us to avoid the information being modifed by anyone else. It's one of the security system principles - authentication principle.

1.2 RSA gen .pem file(key pairs)

One of the RSA algorithm key points is the key that includes the public key and private key which are called key pairs collectively. The key is irregular string in nature. You can assign the string in your code directly, also you can save it in a "pem" file that is standard key information carrier.

So the first step is gen key pairs for RSA algorithm. This paper and code base openssl EVP C-lauguage interface rather than openssl cmd mode. You should compile the openssl source code on your pc, and source code can be downloaded on https://github.com/openssl/openssl/tags website. I recommand the openssl 1.1.1 that no any letter in the version, because of sm2 supported.

The RSA pem file have different format, that are pkcs#1, pkcs#8 as follows:

1.2.1 PKCS#1 Format

The RSA public key PEM file is specific for RSA keys.

It starts with the tags:

-----BEGIN RSA PUBLIC KEY-----

and it ends with the tags:

------END RSA PUBLIC KEY-----

What's more the PEM have base64 encoded data format that still is PKCS#1

That the format with the tags:

RSAPublicKey ::= SEQUENCE {
  modulus INTEGER, -- n
  publicExponent INTEGER -- e
}

The RSA privae key PEM file is basically same with public key PEM file. It's just different from the public key PEM file is PUBLIC string to PRIVATE string.

-----BEGIN RSA PUBLIC KEY-----

...content

------END RSA PUBLIC KEY-----

1.2.2 PKCS#8 Format

Because RSA is not used exclusively inside X509 and SSL/TLS, a more generic key format is available in the form of PKCS#8, that identifies the type of public key and containsum the relevant data.

The difference between the PKCS#1 and PKCS#8 in content respect is that the PKCS#8 not existing RSA words.

-----BEGIN PUBLIC KEY-----

...content

------END PUBLIC KEY-----

The private key PEM file same as the public one.

**So, when we read key information from the PEM file, must ensure that the PKCS is matching. **

1.2.3 How to generate RSA key

How to generate the RSA key pairs using the C-language. Firstly, Some header files should be included in your project before using the openssl EVP interface.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <openssl/ssl.h>
#include <openssl/md5.h>
#include <openssl/evp.h>
#include <openssl/rsa.h>
#include <openssl/bn.h>
#include <openssl/err.h>
#include <openssl/x509.h>

And gen the PEM file need to prepared some essential information that contains the RSA key length, random seed, and path and name of the PEM file.

#define RSA_KEY_LENGTH 1024
static const char rnd_seed[] = "string to make the random number generator initialized";
#define PRIVATE_RSA_KEY_FILE "fotaprikey.pem"
#define PUBLIC_RSA_KEY_FILE "fotapubkey.pem"

There is the code that generate RSA key file. In the code, RSA structure is defined for including all the RSA algorithm information that are large prime number P-value, Q-value etc. All the PEM file operation which is took on BIO struct, using the BIO_new() BIO_write_file etc. The key information will be read from RSA structure to BIO function, finally, genarate a pairs of PEM file for public key and private key.

int generate_rsa_key_files(const char *pub_keyfile, const char *pri_keyfile,
		const unsigned char *passwd, int passwd_len)
{
	RSA *rsa = NULL;
	RAND_seed(rnd_seed, sizeof(rnd_seed));
	rsa = RSA_generate_key(RSA_KEY_LENGTH, RSA_F4, NULL, NULL);
	if(rsa == NULL) {
		printf("RSA_generate_key error!\n");
		return -1;
	}
	BIO *bp = BIO_new(BIO_s_file());
	if (NULL == bp) {
		printf("generate_key bio file new error!\n");
		return -1;
	}
	if(BIO_write_filename(bp, (void *)pub_keyfile) <= 0) {
		printf("BIO_write_filename error!\n");
		return -1;
	}
	if(PEM_write_bio_RSAPublicKey(bp, rsa) != 1) {
		printf("PEM_write_bio_RSAPublicKey error!\n");
		return -1;
	}
	printf("Create rsa public key ok!\n");
	BIO_free_all(bp);
	bp = BIO_new_file(pri_keyfile, "w+");
	if(NULL == bp){
		printf("generate_key bio file new error2!\n");
		return -1;
	}
	if(PEM_write_bio_RSAPrivateKey(bp, rsa,
			EVP_des_ede3_ofb(), (unsigned char *)passwd,
			passwd_len, NULL, NULL) != 1) {
		printf("PEM_write_bio_RSAPublicKey error!\n");
		return -1;
	}
	printf("Create rsa private key ok!\n");
	BIO_free_all(bp);
	RSA_free(rsa);

	return 0;
}

You should notice that function PEM_wirte_bio_RSAPriateKey(), private key can be assigned the password. If you set the password for private key, everyone try to read the private PEM file, the password need to be inputted as required.

And the other tips you should notice is that bio interface will gen the PKCS#1 PEM files. If you want to gen the PKCS#8 format PEM files, you can use PEM_write_bio_PKCS8PrivateKey() interface to implement it.

1.3 RSA encrypt

Using the RSA to encrypt message, I abstract it to openssl_evp_rsa_encrypt function that need user to transform plaintext, ciphertext buffer, and public key PEM file.

The flow of the function is check user input -> read public key from PEM file to EVP_PKEY structure -> using the EVP_PKEY structure do message encrypt.

/*openssl rsa cipher evp using*/
int openssl_evp_rsa_encrypt(	unsigned char *plain_text, size_t plain_len,
								unsigned char *cipher_text, size_t *cipher_len,
								unsigned char *pem_file)
{
	int ret = 0;
	RSA *rsa = NULL;
	EVP_PKEY* public_evp_key = NULL;
	FILE *fp = NULL;
	BIO *bp = NULL;
	EVP_PKEY_CTX *ctx = NULL;

	/*Check the user input.*/
	if (plain_text == NULL || plain_len == 0 || cipher_text == NULL || *cipher_len == 0) {
		printf("input parameters error, plain_text cipher_text or plain_len is NULL or 0.\n");
		ret = -1;
		goto finish;
	}
	if (NULL == pem_file) {
		printf("input pem_file name is invalid\n");
		ret = -1;
		goto finish;
	}
	fp = fopen((const char*)pem_file, "r");
	if (NULL == fp) {
		printf("input pem_file is not exit.\n");
		ret = -1;
		goto finish;
	}
	fclose(fp);
	fp = NULL;

	//OpenSSL_add_all_algorithms();
	bp = BIO_new(BIO_s_file());
	if (bp == NULL) {
		printf("BIO_new is failed.\n");
		ret = -1;
		goto finish;
	}
	/*read public key from pem file.*/
	ret = BIO_read_filename(bp, pem_file);
	rsa = PEM_read_bio_RSAPublicKey(bp, NULL, NULL, NULL);
	if (rsa == NULL) {
		ret = -1;
		printf("open_public_key failed to PEM_read_bio_RSAPublicKey Failed, ret=%d\n", ret);
		goto finish;
	}
	public_evp_key = EVP_PKEY_new();
	if (public_evp_key == NULL) {
		ret = -1;
		printf("open_public_key EVP_PKEY_new failed\n");
		goto finish;
	}
	EVP_PKEY_assign_RSA(public_evp_key, rsa);

	/*do cipher.*/
	ctx = EVP_PKEY_CTX_new(public_evp_key, NULL);
	if (ctx == NULL) {
		ret = -1;
		printf("EVP_PKEY_CTX_new failed\n");
		goto finish;
	}
	ret = EVP_PKEY_encrypt_init(ctx);
	if (ret < 0) {
		printf("ras_pubkey_encrypt failed to EVP_PKEY_encrypt_init. ret = %d\n", ret);
		goto finish;
	}
	ret = EVP_PKEY_CTX_set_rsa_padding(ctx, EVP_PADDING_PKCS7);
	if (ret != 1) {
		printf("EVP_PKEY_CTX_set_rsa_padding failed. ret = %d\n", ret);
		goto finish;
	}
	ret = EVP_PKEY_encrypt(ctx, cipher_text, cipher_len, plain_text, plain_len);
	if (ret < 0) {
		printf("ras_pubkey_encrypt failed to EVP_PKEY_encrypt. ret = %d\n", ret);
		goto finish;
	}
	ret = 0;

finish:
	if (public_evp_key != NULL)
		EVP_PKEY_free(public_evp_key);
	if (bp != NULL)
		BIO_free(bp);
	if (ctx != NULL)
		EVP_PKEY_CTX_free(ctx);

	return ret;
}

1.4 RSA decrypt

Same as encryption procession, the decryption is revert procession of encryption. The funtion as follow need transform ciphertext and plaintext buffer, and private key PEM file is required by decrypting. When you generate the private key PEM file assigned a password, the password should be transformed in the function, if not, a NULL (void*0) parameter is filled in this position.

/*openssl rsa decrypt evp using*/
int openssl_evp_rsa_decrypt(unsigned char *cipher_text, size_t cipher_len,
							   unsigned char *plain_text, size_t *plain_len,
							   const unsigned char *pem_file, const unsigned char *passwd)
{
	int ret = 0;
	size_t out_len = 0;
	EVP_PKEY* private_evp_key = NULL;
	RSA *rsa = NULL;
	BIO *bp = NULL;
	FILE *fp = NULL;
	EVP_PKEY_CTX *ctx = NULL;

	/*Check the user input.*/
	if (plain_text == NULL || *plain_len == 0 || cipher_text == NULL || cipher_len == 0) {
		printf("input parameters error, plain_text cipher_text or plain_len is NULL or 0.\n");
		ret = -1;
		goto finish;
	}
	if (NULL == pem_file) {
		printf("input pem_file name is invalid\n");
		ret = -1;
		goto finish;
	}
	fp = fopen((const char*)pem_file, "r");
	if (NULL == fp) {
		printf("input pem_file is not exit.\n");
		ret = -1;
		goto finish;
	}
	fclose(fp);
	fp = NULL;

	//OpenSSL_add_all_algorithms();
	bp = BIO_new(BIO_s_file());
	if (bp == NULL) {
		printf("BIO_new is failed.\n");
		ret = -1;
		goto finish;
	}
	/*read private key from pem file.*/
	ret = BIO_read_filename(bp, pem_file);
	rsa = PEM_read_bio_RSAPrivateKey(bp, &rsa, NULL, (void*)passwd);
	if (rsa == NULL) {
		ret = -1;
		printf("open_private_key failed to PEM_read_bio_RSAPrivateKey Failed, ret=%d\n", ret);
		goto finish;
	}
	private_evp_key = EVP_PKEY_new();
	if (private_evp_key == NULL) {
		ret = -1;
		printf("open_private_key EVP_PKEY_new failed\n");
		goto finish;
	}
	EVP_PKEY_assign_RSA(private_evp_key, rsa);
	/*do cipher.*/
	ctx = EVP_PKEY_CTX_new(private_evp_key, NULL);
	if (ctx == NULL) {
		ret = -1;
		printf("EVP_PKEY_CTX_new failed\n");
		goto finish;
	}
	ret = EVP_PKEY_decrypt_init(ctx);
	if (ret != 1) {
		printf("rsa_private_key decrypt failed to EVP_PKEY_decrypt_init. ret = %d\n", ret);
		goto finish;
	}
	ret = EVP_PKEY_CTX_set_rsa_padding(ctx, EVP_PADDING_PKCS7);
	if (ret != 1) {
		printf("EVP_PKEY_CTX_set_rsa_padding failed. ret = %d\n", ret);
		goto finish;
	}
	/* Determine buffer length */
	ret = EVP_PKEY_decrypt(ctx, NULL, &out_len, cipher_text, cipher_len);
	if (ret != 1) {
		printf("rsa_prikey_decrypt failed to EVP_PKEY_decrypt. ret = %d\n", ret);
		goto finish;
	}
	*plain_len = out_len;
	ret = EVP_PKEY_decrypt(ctx, plain_text, plain_len, cipher_text, cipher_len);
	if (ret != 1) {
		printf("rsa_prikey_decrypt failed to EVP_PKEY_decrypt. ret = %d\n", ret);
		goto finish;
	}
	ret = 0;
finish:
	if (private_evp_key != NULL)
		EVP_PKEY_free(private_evp_key);
	if (bp != NULL)
		BIO_free(bp);
	if (ctx != NULL)
		EVP_PKEY_CTX_free(ctx);

	return ret;
}

1.5 RSA En/Decryption Experiment

I designed a test case for testing the RSA En/Decryption functions. Firstly, generate a pairs of public private key, then using the encryption function to encrypt the "hello carlos!" message, finally, clear the plaintext buffer, and transform it to decryption function and check the result of decryption wether same to original plaintext.

My environment as follow:

  • System OS: MacOS Catalina 10.15.6
  • OpenSSL Version: 1.1.1
  • Compiler: clang-gcc
  • IDE: Clion - cmake

I wrote the test code as folllows:

int test_evp_rsa_encrypt_decrypt()
{
	int ret = 0, i = 0;
	unsigned char cipher_out[1024];
	unsigned char plain_in[] = "hello carlos rsa...";
	size_t out_len = 1024;
	size_t in_len = strlen(plain_in);

	ret = openssl_evp_rsa_encrypt(plain_in, in_len, cipher_out, &out_len, PUBLIC_RSA_KEY_FILE);
	if (ret != 0) {
		printf("error in encrypt %d\n", ret);
	}
	printf("rsa plain text is : %s \n", plain_in);
	printf("rsa cipher len = %d text is :\n", out_len);
	for (i = 0; i < out_len; i ++) {
		printf("%02X", cipher_out[i]);
	}
	printf("\n");
	memset(plain_in, '\0', in_len);
	in_len = 1;
	ret = openssl_evp_rsa_decrypt(cipher_out, out_len, plain_in, &in_len, PRIVATE_RSA_KEY_FILE, "12345");
	if (ret != 0) {
		printf("error in decrypt %d\n", ret);
	}
	printf("rsa decrypt len = %d  and text is : %s \n", in_len, plain_in);
	return ret;
}

int test_evp_sm2_encrypt_decrypt()
{
	int ret = 0, i = 0;
	unsigned char cipher_out[1024];
	unsigned char plain_in[] = "hello carlos sm2...";
	size_t out_len = 1024;
	size_t in_len = strlen(plain_in);

	ret = openssl_evp_sm2_encrypt(plain_in, strlen(plain_in), cipher_out, &out_len, PUBLIC_SM2_KEY_FILE);
	if (ret != 0) {
		printf("error in encrypt %d\n", ret);
		return ret;
	}
	printf("sm2 plain text is : %s \n", plain_in);
	printf("sm2 cipher len = %d text is :\n", out_len);
	for (i = 0; i < out_len; i ++) {
		printf("%02X", cipher_out[i]);
	}
	printf("\n");
	memset(plain_in, '\0', in_len);
	in_len = 1;
	ret = openssl_evp_sm2_decrypt(cipher_out, out_len, plain_in, &in_len, PRIVATE_SM2_KEY_FILE, "12345");
	if (ret != 0) {
		printf("error in decrypt %d\n", ret);
	}
	printf("sm2 decrypt len = %d  and text is : %s \n", in_len, plain_in);
	return ret;
}

image

1.6 RSA signature

RSA sign procession is different from encryption and decryption, that is contains other asymmetric algorithm, Message Digest and Secure Hash Algorithm. And anther difference is that finishing the signature need the private key. It's the opposite of the RSA encryption/decryption process.

// RSA_PKCS1_PADDING  RSA_OAEP_PADDING
int openssl_evp_rsa_signature(unsigned char *sign_rom, size_t sign_rom_len,
								unsigned char *result, size_t *result_len,
								const unsigned char *priv_pem_file, const unsigned char *passwd)
{
	int ret = 0;
	FILE *fp = NULL;
	EVP_PKEY* private_evp_key = NULL;
	RSA *rsa = NULL;
	BIO *bp = NULL;
	EVP_PKEY_CTX *ctx = NULL;
	EVP_MD_CTX *evp_md_ctx = NULL;

	/*Check the user input.*/
	if (sign_rom == NULL || sign_rom_len == 0 || result == NULL || *result_len == 0) {
		printf("input parameters error, content or len is NULL or 0.\n");
		ret = -1;
		goto finish;
	}
	if (NULL == priv_pem_file) {
		printf("input pem_file name is invalid\n");
		ret = -1;
		return ret;
	}
	fp = fopen((const char*)priv_pem_file, "r");
	if (NULL == fp) {
		printf("input pem_file is not exit.\n");
		ret = -1;
		goto finish;
	}
	fclose(fp);
	fp = NULL;
	/*read private key from pem file to private_evp_key*/
	//OpenSSL_add_all_algorithms();
	bp = BIO_new(BIO_s_file());
	if (bp == NULL) {
		printf("BIO_new is failed.\n");
		ret = -1;
		goto finish;
	}
	ret = BIO_read_filename(bp, priv_pem_file);
	rsa = PEM_read_bio_RSAPrivateKey(bp, &rsa, NULL, (void*)passwd);
	if (rsa == NULL) {
		ret = -1;
		printf("open_private_key failed to PEM_read_bio_RSAPrivateKey Failed, ret=%d\n", ret);
		goto finish;
	}
	private_evp_key = EVP_PKEY_new();
	if (private_evp_key == NULL) {
		ret = -1;
		printf("open_private_key EVP_PKEY_new failed\n");
		goto finish;
	}
	EVP_PKEY_assign_RSA(private_evp_key, rsa);
	/*do signature*/
	evp_md_ctx = EVP_MD_CTX_new();
	if (evp_md_ctx == NULL) {
		printf("EVP_MD_CTX_new failed.\n");
		ret = -1;
		goto finish;
	}
	EVP_MD_CTX_init(evp_md_ctx);
	ret = EVP_SignInit_ex(evp_md_ctx, EVP_md5(), NULL);
	if (ret != 1) {
		printf("EVP_SignInit_ex failed, ret = %d\n", ret);
		goto finish;
	}
	ret = EVP_SignUpdate(evp_md_ctx, sign_rom, sign_rom_len);
	if (ret != 1) {
		printf("EVP_SignUpdate failed, ret = %d\n", ret);
		goto finish;
	}
	ret = EVP_SignFinal(evp_md_ctx, result, (unsigned int*)result_len, private_evp_key);
	if (ret != 1) {
		printf("EVP_SignFinal failed, ret = %d\n", ret);
		goto finish;
	}
	ret = 0;
finish:
	if (private_evp_key != NULL)
		EVP_PKEY_free(private_evp_key);
	if (bp != NULL)
		BIO_free(bp);
	if (evp_md_ctx != NULL)
		EVP_MD_CTX_free(evp_md_ctx);
	if (ctx != NULL)
		EVP_PKEY_CTX_free(ctx);
	return ret;
}

The second parameter in function EVP_SignInit_ex(evp_md_ctx, EVP_md5(), NULL); is sub-algorithm of RSA sign, multiple message digest and secure hash algorithm are available applying the RSA.

1.7 RSA verify

RSA verify just return a bool result to you that it's either ture or false. If the true, it's surely verified message success, and vice versa.

int openssl_evp_rsa_verify(unsigned char *sign_rom, size_t sign_rom_len,
							unsigned char *result, size_t result_len,
							const unsigned char *pub_pem_file)
{
	int ret = 0;
	FILE *fp = NULL;
	EVP_PKEY* public_evp_key = NULL;
	RSA *rsa = NULL;
	BIO *bp = NULL;
	EVP_PKEY_CTX *ctx = NULL;
	EVP_MD_CTX *evp_md_ctx = NULL;

	/*Check the user input.*/
	if (sign_rom == NULL || sign_rom_len == 0 || result == NULL || result_len == 0) {
		printf("input parameters error, content or len is NULL or 0.\n");
		ret = -1;
		goto finish;
	}
	if (NULL == pub_pem_file) {
		printf("input pem_file name is invalid\n");
		ret = -1;
		goto finish;
	}
	fp = fopen((const char*)pub_pem_file, "r");
	if (NULL == fp) {
		printf("input pem_file is not exit.\n");
		ret = -1;
		goto finish;
	}
	fclose(fp);
	fp = NULL;
	/*read public key from pem file to private_evp_key*/
	//OpenSSL_add_all_algorithms();
	bp = BIO_new(BIO_s_file());
	if (bp == NULL) {
		printf("BIO_new is failed.\n");
		ret = -1;
		goto finish;
	}
	ret = BIO_read_filename(bp, pub_pem_file);
	rsa = PEM_read_bio_RSAPublicKey(bp, NULL, NULL, NULL);
	if (rsa == NULL) {
		ret = -1;
		printf("open_public_key failed to PEM_read_bio_RSAPublicKey Failed, ret=%d\n", ret);
		goto finish;
	}
	public_evp_key = EVP_PKEY_new();
	if (public_evp_key == NULL) {
		ret = -1;
		goto finish;
	}
	EVP_PKEY_assign_RSA(public_evp_key, rsa);
	/*do verify*/
	evp_md_ctx = EVP_MD_CTX_new();
	if (evp_md_ctx == NULL) {
		printf("EVP_MD_CTX_new failed.\n");
		ret = -1;
		goto finish;
	}
	EVP_MD_CTX_init(evp_md_ctx);
	ret = EVP_VerifyInit_ex(evp_md_ctx, EVP_md5(), NULL);
	if (ret != 1) {
		printf("EVP_VerifyInit_ex failed, ret = %d\n", ret);
		goto finish;
	}
	ret = EVP_VerifyUpdate(evp_md_ctx, result, result_len);
	if (ret != 1) {
		printf("EVP_VerifyUpdate failed, ret = %d\n", ret);
		goto finish;
	}
	ret = EVP_VerifyFinal(evp_md_ctx, sign_rom, (unsigned int)sign_rom_len, public_evp_key);
	if (ret != 1) {
		printf("EVP_VerifyFinal failed, ret = %d\n", ret);
		goto finish;
	}
	ret = 0;
finish:
	if (public_evp_key != NULL)
		EVP_PKEY_free(public_evp_key);
	if (bp != NULL)
		BIO_free(bp);
	if (evp_md_ctx != NULL)
		EVP_MD_CTX_free(evp_md_ctx);
	if (ctx != NULL)
		EVP_PKEY_CTX_free(ctx);

	return ret;
}

1.8 RSA Sign/Verify Experment

I have prepared test code for RSA and Verify similarly. Firstly, gave the message to add signature information using the private key. Secondly, I transformed the signature information and origin message to verify funtion. Finally, the verify funtion return me a boolean result told me the status verified.

int test_evp_rsa_signature_verify()
{
	int ret = 0, i = 0;
	unsigned char sign_out[1024];
	unsigned char plain_in[] = "hello carlos.";
	size_t out_len = 256;
	size_t in_len = strlen(plain_in);

	ret = openssl_evp_rsa_signature(plain_in, in_len, sign_out, &out_len, PRIVATE_RSA_KEY_FILE, "12345");
	if (ret != 0) {
		printf("rsa signature failed!\n");
		return ret;
	}
	printf("rsa %s openssl sign len = %d, signature result: \n", plain_in, out_len);
	for(i = 0; i < out_len; i++) {
		printf("%02X", sign_out[i]);
	}
	printf("\n");

	ret = openssl_evp_rsa_verify(sign_out, out_len, plain_in, in_len, PUBLIC_RSA_KEY_FILE);
	if (ret != 0) {
		printf("rsa verify failed!\n");
	} else {
		printf("rsa verify succeed!\n");
	}
}

2. SM2

2.1 What is SM2

SM is the first letter of abbreviations (商密:Shang Mi). It's invented by National Cryptography Department of China. SM2 is one of the algorithm of the SM series cryptography and it belongs to asymmetric cryptography. So there are public key and private key required and SM2 PEM format file of key pairs is meet PKCS#1 PKCS#8 standards.

Using the SM2 in openssl, the #include "openssl_sm2.h" should be written at head of .c file.

2.2 SM2 gen .pem file(key pairs)

SM2 algorithm belongs to the type of ellipse encryption, so when we use SM2 algorithm, we need to set the type of SM2 curve. There are many curves you can select to ellipse encryption. SM2 pem file is different from RSA. This chapter will introduce that how to generate the sm2 .pem file by giving the code examples.

int generate_sm2_key_files(const char *pub_keyfile, const char *pri_keyfile,
                           const unsigned char *passwd, int passwd_len)
{
    int ret = 0;
    EC_KEY *ec_key = NULL;
    EC_GROUP *ec_group = NULL;
#ifdef MAKE_KEY_TO_RAM
    size_t prikey_len = 0;
	size_t pubkey_len = 0;
	unsigned char *prikey_buffer = NULL;
	unsigned char *pubkey_buffer = NULL;
#endif
    BIO *pri_bio = NULL;
    BIO *pub_bio = NULL;

    ec_key = EC_KEY_new();
    if (ec_key == NULL) {
        ret = -1;
        printf("EC_KEY_new() failed return NULL.\n");
        goto finish;
    }
    ec_group = EC_GROUP_new_by_curve_name(NID_sm2);
    if (ec_group == NULL) {
        ret = -1;
        printf("EC_GROUP_new_by_curve_name() failed, return NULL.\n");
        goto finish;
    }
    ret = EC_KEY_set_group(ec_key, ec_group);
    if (ret != 1) {
        printf("EC_KEY_set_group() failed, ret = %d\n", ret);
        ret = -1;
        goto finish;
    }
    ret = EC_KEY_generate_key(ec_key);
    if (!ret) {
        printf("EC_KEY_generate_key() failed, ret = %d\n", ret);
        ret = -1;
        goto finish;
    }
    printf("Create sm2 private key ok!");
#ifdef MAKE_KEY_TO_RAM
    pri_bio = BIO_new(BIO_s_mem());
#else
    pri_bio = BIO_new(BIO_s_file());
#endif
    if (pri_bio == NULL) {
        ret = -1;
        printf("pri_bio = BIO_new(BIO_s_file()) failed, return NULL. \n");
        goto finish;
    }
    ret = BIO_write_filename(pri_bio, (void *)pri_keyfile);
    if (ret <= 0) {
        printf("BIO_write_filename error!\n");
        goto finish;
    }
    ret = PEM_write_bio_ECPrivateKey(pri_bio, ec_key, NULL, (unsigned char *)passwd, passwd_len, NULL, NULL);
    if (ret != 1) {
        printf("PEM_write_bio_ECPrivateKey error! ret = %d \n", ret);
        ret = -1;
        goto finish;
    }
#ifdef MAKE_KEY_TO_RAM
    pub_bio = BIO_new(BIO_s_mem());
#else
    pub_bio = BIO_new(BIO_s_file());
#endif
    if (pub_bio == NULL) {
        ret = -1;
        printf("pub_bio = BIO_new(BIO_s_file()) failed, return NULL. \n");
        goto finish;
    }
    ret = BIO_write_filename(pub_bio, (void *)pub_keyfile);
    if (ret <= 0) {
        printf("BIO_write_filename error!\n");
        goto finish;
    }
    ret = PEM_write_bio_EC_PUBKEY(pub_bio, ec_key);
    if (ret != 1) {
        ret = -1;
        printf("PEM_write_bio_EC_PUBKEY error!\n");
        goto finish;
    }
    printf("Create sm2 public key ok!");
#ifdef MAKE_KEY_TO_RAM
    PEM_write_bio_EC_PUBKEY(pub_bio, ec_key);
	prikey_len = BIO_pending(pri_bio);
	pubkey_len = BIO_Pending(pub_bio);
	prikey_buffer = (unsigned char*)OPENSSL_malloc((prikey_len + 1) * sizeof(unsigned char));
	if (prikey_buffer == NULL) {
		ret = -1;
		printf("prikey_buffer OPENSSL_malloc failed, return NULL. \n");
		goto finish;
	}
	pubkey_buffer = (unsigned char*)OPENSSL_malloc((pubkey_len + 1) * sizeof(unsigned char));
	if (pubkey_buffer == NULL) {
		ret = -1;
		printf("pubkey_buffer OPENSSL_malloc failed, return NULL. \n");
		goto finish;
	}
	BIO_read(pri_bio, prikey_buffer, prikey_len);
	BIO_read(pub_bio, pubkey_buffer, pubkey_len);
	prikey_buffer[prikey_len] = '\0';
	pubkey_buffer[pubkey_len] = '\0';
#endif
    finish:
    if (ec_key != NULL)
        EC_KEY_free(ec_key);
    if (ec_group != NULL)
        EC_GROUP_free(ec_group);
#ifdef MAKE_KEY_TO_RAM
    if (prikey_buffer != NULL)
		OPENSSL_free(prikey_buffer);
	if (pubkey_buffer != NULL)
		OPENSSL_free(pubkey_buffer);
#endif
    if (pub_bio != NULL)
        BIO_free_all(pub_bio);
    if (pri_bio != NULL)
        BIO_free_all(pri_bio);
    return ret;
}

2.3 SM2 encrypt

/*openssl sm2 cipher evp using*/
int openssl_evp_sm2_encrypt(	unsigned char *plain_text, size_t plain_len,
                                unsigned char *cipher_text, size_t *cipher_len,
                                unsigned char *pem_file)
{
    int ret = 0;
    size_t out_len = 512;
    unsigned char cipper[512];
    FILE *fp = NULL;
    BIO *bp = NULL;
    EC_KEY *ec_key = NULL;
    EVP_PKEY* public_evp_key = NULL;
    EVP_PKEY_CTX *ctx = NULL;

    /*Check the user input.*/
    if (plain_text == NULL || plain_len == 0 || cipher_text == NULL || *cipher_len == 0) {
        printf("input parameters error, plain_text cipher_text or plain_len is NULL or 0.\n");
        ret = -1;
        return ret;
    }
    if (NULL == pem_file) {
        printf("input pem_file name is invalid\n");
        ret = -1;
        return ret;
    }
    fp = fopen(pem_file, "r");
    if (NULL == fp) {
        printf("input pem_file is not exit.\n");
        ret = -1;
        return ret;
    }
    fclose(fp);
    fp = NULL;

    //OpenSSL_add_all_algorithms();
    bp = BIO_new(BIO_s_file());
    if (bp == NULL) {
        printf("BIO_new is failed.\n");
        ret = -1;
        return ret;
    }
    /*read public key from pem file.*/
    ret = BIO_read_filename(bp, pem_file);
    ec_key = PEM_read_bio_EC_PUBKEY(bp, NULL, NULL, NULL);
    if (ec_key == NULL) {
        ret = -1;
        printf("open_public_key failed to PEM_read_bio_EC_PUBKEY Failed, ret=%d\n", ret);
        goto finish;
    }
    public_evp_key = EVP_PKEY_new();
    if (public_evp_key == NULL) {
        ret = -1;
        printf("open_public_key EVP_PKEY_new failed\n");
        goto finish;
    }
    ret = EVP_PKEY_set1_EC_KEY(public_evp_key, ec_key);
    if (ret != 1) {
        ret = -1;
        printf("EVP_PKEY_set1_EC_KEY failed\n");
        goto finish;
    }
    ret = EVP_PKEY_set_alias_type(public_evp_key, EVP_PKEY_SM2);
    if (ret != 1) {
        printf("EVP_PKEY_set_alias_type to EVP_PKEY_SM2 failed! ret = %d\n", ret);
        ret = -1;
        goto finish;
    }
    /*modifying a EVP_PKEY to use a different set of algorithms than the default.*/

    /*do cipher.*/
    ctx = EVP_PKEY_CTX_new(public_evp_key, NULL);
    if (ctx == NULL) {
        ret = -1;
        printf("EVP_PKEY_CTX_new failed\n");
        goto finish;
    }
    ret = EVP_PKEY_encrypt_init(ctx);
    if (ret < 0) {
        printf("sm2_pubkey_encrypt failed to EVP_PKEY_encrypt_init. ret = %d\n", ret);
        EVP_PKEY_free(public_evp_key);
        EVP_PKEY_CTX_free(ctx);
        return ret;
    }
    ret = EVP_PKEY_encrypt(ctx, cipher_text, cipher_len, plain_text, plain_len);
    if (ret < 0) {
        printf("sm2_pubkey_encrypt failed to EVP_PKEY_encrypt. ret = %d\n", ret);
        EVP_PKEY_free(public_evp_key);
        EVP_PKEY_CTX_free(ctx);
        return ret;
    }
    ret = 0;
    finish:
    if (public_evp_key != NULL)
        EVP_PKEY_free(public_evp_key);
    if (ctx != NULL)
        EVP_PKEY_CTX_free(ctx);
    if (bp != NULL)
        BIO_free(bp);
    if (ec_key != NULL)
        EC_KEY_free(ec_key);

    return ret;
}

It's different from the RSA encryption is there is a ec_key need to set except evp_key, the ec_key need be assigned by EVP_PKEY_set1_EC_KEY and using the EVP_PKEY_set_alias_type to set which curve is your select.(SM2 is EVP_PKEY_SM2 macro define)

2.4 SM2 decrypt

/*openssl sm2 decrypt evp using*/
int openssl_evp_sm2_decrypt(unsigned char *cipher_text, size_t cipher_len,
                            unsigned char *plain_text, size_t *plain_len,
                            const unsigned char *pem_file, const unsigned char *passwd)
{
    int ret = 0;
    size_t out_len = 0;
    FILE *fp = NULL;
    BIO *bp = NULL;
    EC_KEY *ec_key = NULL;
    EVP_PKEY* private_evp_key = NULL;
    EVP_PKEY_CTX *ctx = NULL;

    /*Check the user input.*/
    if (plain_text == NULL || cipher_len == 0 || cipher_text == NULL || *plain_len == 0) {
        printf("input parameters error, plain_text cipher_text or plain_len is NULL or 0.\n");
        ret = -1;
        return ret;
    }
    if (NULL == pem_file) {
        printf("input pem_file name is invalid\n");
        ret = -1;
        return ret;
    }
    fp = fopen(pem_file, "r");
    if (NULL == fp) {
        printf("input pem_file is not exit.\n");
        ret = -1;
        return ret;
    }
    fclose(fp);
    fp = NULL;

    //OpenSSL_add_all_algorithms();
    bp = BIO_new(BIO_s_file());
    if (bp == NULL) {
        printf("BIO_new is failed.\n");
        ret = -1;
        return ret;
    }
    /*read public key from pem file.*/
    ret = BIO_read_filename(bp, pem_file);
    ec_key = PEM_read_bio_ECPrivateKey(bp, &ec_key, NULL, (void*)passwd);
    if (ec_key == NULL) {
        ret = -1;
        printf("open_private_key failed to PEM_read_bio_ECPrivateKey Failed, ret=%d\n", ret);
        goto finish;
    }
    private_evp_key = EVP_PKEY_new();
    if (private_evp_key == NULL) {
        ret = -1;
        printf("open_public_key EVP_PKEY_new failed\n");
        goto finish;
    }
    ret = EVP_PKEY_set1_EC_KEY(private_evp_key, ec_key);
    if (ret != 1) {
        ret = -1;
        printf("EVP_PKEY_set1_EC_KEY failed\n");
        goto finish;
    }
    ret = EVP_PKEY_set_alias_type(private_evp_key, EVP_PKEY_SM2);
    if (ret != 1) {
        printf("EVP_PKEY_set_alias_type to EVP_PKEY_SM2 failed! ret = %d\n", ret);
        ret = -1;
        goto finish;
    }
    /*modifying a EVP_PKEY to use a different set of algorithms than the default.*/

    /*do cipher.*/
    ctx = EVP_PKEY_CTX_new(private_evp_key, NULL);
    if (ctx == NULL) {
        ret = -1;
        printf("EVP_PKEY_CTX_new failed\n");
        goto finish;
    }
    ret = EVP_PKEY_decrypt_init(ctx);
    if (ret < 0) {
        printf("sm2 private_key decrypt failed to EVP_PKEY_decrypt_init. ret = %d\n", ret);
        goto finish;
    }
    /* Determine buffer length */
    ret = EVP_PKEY_decrypt(ctx, NULL, &out_len, cipher_text, cipher_len);
    if (ret < 0) {
        printf("sm2_prikey_decrypt failed to EVP_PKEY_decrypt. ret = %d\n", ret);
        goto finish;
    }
    *plain_len = out_len;
    ret = EVP_PKEY_decrypt(ctx, plain_text, plain_len, cipher_text, cipher_len);
    if (ret < 0) {
        printf("sm2_prikey_decrypt failed to EVP_PKEY_decrypt. ret = %d\n", ret);
        goto finish;
    }
    ret = 0;
    finish:
    if (private_evp_key != NULL)
        EVP_PKEY_free(private_evp_key);
    if (ctx != NULL)
        EVP_PKEY_CTX_free(ctx);
    if (bp != NULL)
        BIO_free(bp);
    if (ec_key != NULL)
        EC_KEY_free(ec_key);
    return ret;
}

2.5 SM2 signature

int openssl_evp_sm2_signature(unsigned char *sign_rom, size_t sign_rom_len,
                              unsigned char *result, size_t *result_len,
                              const unsigned char *priv_pem_file, const unsigned char *passwd)
{
    int ret = 0;
    size_t out_len = 0;
    FILE *fp = NULL;
    BIO *bp = NULL;
    EC_KEY *ec_key = NULL;
    EVP_PKEY* private_evp_key = NULL;
    EVP_PKEY_CTX *ctx = NULL;
    EVP_MD_CTX *evp_md_ctx = NULL;

    /*Check the user input.*/
    if (sign_rom == NULL || sign_rom_len == 0 || result == NULL || *result_len == 0) {
        printf("input parameters error, plain_text cipher_text or plain_len is NULL or 0.\n");
        ret = -1;
        return ret;
    }
    if (NULL == priv_pem_file) {
        printf("input pem_file name is invalid\n");
        ret = -1;
        return ret;
    }
    fp = fopen(priv_pem_file, "r");
    if (NULL == fp) {
        printf("input pem_file is not exit.\n");
        ret = -1;
        return ret;
    }
    fclose(fp);
    fp = NULL;

    //OpenSSL_add_all_algorithms();
    bp = BIO_new(BIO_s_file());
    if (bp == NULL) {
        printf("BIO_new is failed.\n");
        ret = -1;
        return ret;
    }
    /*read public key from pem file.*/
    ret = BIO_read_filename(bp, priv_pem_file);
    ec_key = PEM_read_bio_ECPrivateKey(bp, &ec_key, NULL, (void*)passwd);
    if (ec_key == NULL) {
        ret = -1;
        printf("open_private_key failed to PEM_read_bio_ECPrivateKey Failed, ret=%d\n", ret);
        goto finish;
    }
    private_evp_key = EVP_PKEY_new();
    if (private_evp_key == NULL) {
        ret = -1;
        printf("open_public_key EVP_PKEY_new failed\n");
        goto finish;
    }
    ret = EVP_PKEY_set1_EC_KEY(private_evp_key, ec_key);
    if (ret != 1) {
        ret = -1;
        printf("EVP_PKEY_set1_EC_KEY failed\n");
        goto finish;
    }
    ret = EVP_PKEY_set_alias_type(private_evp_key, EVP_PKEY_SM2);
    if (ret != 1) {
        printf("EVP_PKEY_set_alias_type to EVP_PKEY_SM2 failed! ret = %d\n", ret);
        ret = -1;
        goto finish;
    }
    /*modifying a EVP_PKEY to use a different set of algorithms than the default.*/

    /*do signature.*/
    evp_md_ctx = EVP_MD_CTX_new();
    if (evp_md_ctx == NULL) {
        printf("EVP_MD_CTX_new failed.\n");
        ret = -1;
        goto finish;
    }
    EVP_MD_CTX_init(evp_md_ctx);
    ret = EVP_SignInit_ex(evp_md_ctx, EVP_sm3(), NULL);
    if (ret != 1) {
        printf("EVP_SignInit_ex failed, ret = %d\n", ret);
        goto finish;
    }
    ret = EVP_SignUpdate(evp_md_ctx, sign_rom, sign_rom_len);
    if (ret != 1) {
        printf("EVP_SignUpdate failed, ret = %d\n", ret);
        goto finish;
    }
    ret = EVP_SignFinal(evp_md_ctx, result, (unsigned int*)result_len, private_evp_key);
    if (ret != 1) {
        printf("EVP_SignFinal failed, ret = %d\n", ret);
        goto finish;
    }
    ret = 0;
    finish:
    if (private_evp_key != NULL)
        EVP_PKEY_free(private_evp_key);
    if (ctx != NULL)
        EVP_PKEY_CTX_free(ctx);
    if (bp != NULL)
        BIO_free(bp);
    if (ec_key != NULL)
        EC_KEY_free(ec_key);
    return ret;
}

2.6 SM2 verify

int openssl_evp_sm2_verify(unsigned char *sign_rom, size_t sign_rom_len,
                           unsigned char *result, size_t result_len,
                           const unsigned char *pub_pem_file)
{
    int ret = 0;
    FILE *fp = NULL;
    BIO *bp = NULL;
    EVP_MD_CTX *evp_md_ctx = NULL;
    EC_KEY *ec_key = NULL;
    EVP_PKEY* public_evp_key = NULL;

    /*Check the user input.*/
    if (sign_rom == NULL || sign_rom_len == 0 || result == NULL || result_len == 0) {
        printf("input parameters error, content or len is NULL or 0.\n");
        ret = -1;
        goto finish;
    }
    if (NULL == pub_pem_file) {
        printf("input pem_file name is invalid\n");
        ret = -1;
        goto finish;
    }
    fp = fopen((const char*)pub_pem_file, "r");
    if (NULL == fp) {
        printf("input pem_file is not exit.\n");
        ret = -1;
        goto finish;
    }
    fclose(fp);
    fp = NULL;
    /*read public key from pem file to private_evp_key*/
    //OpenSSL_add_all_algorithms();
    bp = BIO_new(BIO_s_file());
    if (bp == NULL) {
        printf("BIO_new is failed.\n");
        ret = -1;
        return ret;
    }
    /*read public key from pem file.*/
    ret = BIO_read_filename(bp, pub_pem_file);
    ec_key = PEM_read_bio_EC_PUBKEY(bp, NULL, NULL, NULL);
    if (ec_key == NULL) {
        ret = -1;
        printf("open_public_key failed to PEM_read_bio_EC_PUBKEY Failed, ret=%d\n", ret);
        goto finish;
    }
    public_evp_key = EVP_PKEY_new();
    if (public_evp_key == NULL) {
        printf("open_public_key EVP_PKEY_new failed\n");
        ret = -1;
        goto finish;
    }
    ret = EVP_PKEY_set1_EC_KEY(public_evp_key, ec_key);
    if (ret != 1) {
        ret = -1;
        printf("EVP_PKEY_set1_EC_KEY failed\n");
        goto finish;
    }
    ret = EVP_PKEY_set_alias_type(public_evp_key, EVP_PKEY_SM2);
    if (ret != 1) {
        printf("EVP_PKEY_set_alias_type to EVP_PKEY_SM2 failed! ret = %d\n", ret);
        ret = -1;
        goto finish;
    }
    /*modifying a EVP_PKEY to use a different set of algorithms than the default.*/
    /*do verify*/
    evp_md_ctx = EVP_MD_CTX_new();
    if (evp_md_ctx == NULL) {
        printf("EVP_MD_CTX_new failed.\n");
        ret = -1;
        goto finish;
    }
    EVP_MD_CTX_init(evp_md_ctx);
    ret = EVP_VerifyInit_ex(evp_md_ctx, EVP_sm3(), NULL);
    if (ret != 1) {
        printf("EVP_VerifyInit_ex failed, ret = %d\n", ret);
        ret = -1;
        goto finish;
    }
    ret = EVP_VerifyUpdate(evp_md_ctx, result, result_len);
    if (ret != 1) {
        printf("EVP_VerifyUpdate failed, ret = %d\n", ret);
        ret = -1;
        goto finish;
    }
    ret = EVP_VerifyFinal(evp_md_ctx, sign_rom, (unsigned int)sign_rom_len, public_evp_key);
    if (ret != 1) {
        printf("EVP_VerifyFinal failed, ret = %d\n", ret);
        ret = -1;
        goto finish;
    }
    ret = 0;
    finish:
    if (bp != NULL)
        BIO_free(bp);
    if (evp_md_ctx != NULL)
        EVP_MD_CTX_free(evp_md_ctx);
    if (ec_key != NULL)
        EC_KEY_free(ec_key);
    if (public_evp_key != NULL)
        EVP_PKEY_free(public_evp_key);

    return ret;
}

Code Share

You get the driver code and testcase from my github as follows:

https://github.com/carloscn/cryptography

That is a Clion format project.

Reference List

[1]https://blog.csdn.net/weixin_41761608/article/details/107623909

[2]https://www.openssl.org/docs/man1.1.0/man3/EVP_PKEY_decrypt.html

[3]https://www.openssl.org/docs/man1.1.0/man3/EVP_PKEY_encrypt.html

[4]http://man.sourcentral.org/debian-unstable/3+PEM_read_PUBKEY

[5]https://stackoverflow.com/questions/31482186/generating-a-pem-with-openssl-in-c

[6]https://www.cnblogs.com/cocoajin/p/6134382.html

[7]https://github.com/greendow/SM2-signature-creation-and-verification/

Qt_QtWebkits如何向QtWebEngine过渡

QtWebkits如何向QtWebEngine过渡

posted @ 2017-07-17 13:15 Carlos·Wei

1. 前言

很遗憾,QtWebkits在Qt5.6以上版本被淘汰了,对于这个接口良且和其他类例如QWebFrame完美结合的组件就这么没了,我只能表示惋惜。对于QtWebEngine新的组件,不得不承认它从Chromium继承过来的强大的性能,但接口上还不是很丰富,和其他类的交互也不是很完美,期待Qt能够对其进行进一步开发,我也会不断的升级Qt,尝试新的接口。

目前而言,QWebEngine有以下缺点:

  • MinGW版本的Qt不支持,即便是Qt5.6版本以上也是不支持的。仅仅支持MSVC版本。
  • 接口暂时不丰富
  • 无法和QWebFrame进行交互(使用了新的QWebChannel和QWebEnginePage组合进行交互)

基于我们的GPS定位项目,参考:[Qt开发北斗定位系统融合百度地图API及Qt程序打包发布] ,我们在该项目中使用的是Qt5.5版本,在嵌入的浏览器作为加载地图用的是QWebKits组件,我们将其升级使用QWebEngine进行加载地图,和HTML和JS进行交互。我们以此为例,进行简要的介绍。

2. 两者的UI上面的区别

你刚刚升级到Qt5.6版本可能在UI设计界面时候在组件中找不到QWebEngineView这个组件,无法从这里拖拽这个组件到你的UI界面上。我查阅了很多资料,看到别人经常使用 ui->webEngineview->... 这样,我甚至怀疑是否因为安装了其他版本的Qt影响到了我,我卸载了包含5.6版本的所有Qt,又重新安装了一遍,但是再重启软件后,依然没有发现QWebEngineView这个鬼东西。在Qt5.5中你也能发现有这样的组件QWebView,如图1所示:

Qt旧版本中的WebKits项

QWebView组件可以通过QWebFrame来进行HTML和JS的通信,如果过渡到QWebEngineView,要是没有这个UI组件的话,我如何把浏览器嵌入到软件界面,实现网页和软件的混合编程呢。根据官方提供的一个例子中,cookiebrowser中找到了答案,这也是官方给的例子中,唯一一个嵌入到网页中的!(不得不说,Qt给的例子很模糊很差!) 经过研究, QWebEngineView使用widget组件,拖拉出来是一个透明的组件,对着组件按右键->promote to.. ->选择QWebEngineView,如图2,完成操作。

把Widget promote to 成QWebEngineView组件

有了QWebEngineView这个UI组件,我们可以在程序中调用其成员、方法和函数完成操作了。

3. 使用方法区别

在使用方法上有很大的区别,可以说是两个完全不同理念的东西,这里为了更通俗易懂,就不粘贴API文档中函数解释,就用最常用的!

#include <QtWebEngineWidgets>	// 基本组件
#include <QWebEnginePage>	    // HTML页面
#include <QWebChannel>          // C++和JS/HTML双向通信,代替了已淘汰的QtWebFrame的功能

在我们的项目中一开始就要引入这样的组件,但在我们的项目中,没有频繁用到与JS的互相交互,所以这里暂时没有关于QWebChannel的使用方法,只留下这个接口。

以下为区别:


  • 在WebKits中的初始化:
QUrl url(strMapPath);		// strMapPath为QString类,是你html文件的路径
ui->webView->load(url);
ui->webView->setContentsMargins(0,0,0,0);
ui->webView->setTextSizeMultiplier(1);//设置网页字体大小
connect(ui->webView->page()->mainFrame(), SIGNAL(javaScriptWindowObjectCleared()),
            this, SLOT(slotPopulateJavaScriptWindowObject()));

我们会使用load方法加载html所在的界面,使用QWebFrame类的mainFrame()中的SIGNAL和槽函数

void Widget::slotPopulateJavaScriptWindowObject()
{
    ui->webView->page()->mainFrame()->addToJavaScriptWindowObject("ReinforcePC", this);
}

进行响应。参考文献1:《javascript调用qt》,可以解释这个槽函数的重要性。

  • 在WebEngine中的初始化:
QWebEnginePage *page = new QWebEnginePage(this);  // 定义一个page作为页面管理
QWebChannel *channel = new QWebChannel(this);     // 定义一个channel作为和JS或HTML交互
page->load(strMapPath);							// page上加载html路径
page->setWebChannel(channel);					// 把channel配置到page上,让channel作为其信使
ui->webEngine->setPage(page);					// 建立page和UI上的webEngine的联系

如果你的 初始化程序写到这里,当你运行程序的时候,无论是webKits里的WebView还是新版的webEngineView,你的UI界面上的那个组件区域就会显示那个html文件了。

到此,我们完成了两者的初始化。


  • WebKits组件中的运行JS:

我们以按钮的槽函数为例,当点击按钮时,会向JS发送命令,运行JS脚本,我们这里发送的是将显示变为卫星图的JS命令:

void Widget::on_pushButtonStreetMap_clicked()
{
    QWebFrame *frame = ui->webView->page()->mainFrame(); // 定义一个QWebFrame负责交互
    QString cmd = QString("showStreetMap()"); // JS的命令
    frame->evaluateJavaScript(cmd);			  // 使用frame下的命令运行该命令
}

从这个例子中我们也可以看到,QWebFrame是JS交互的关键。

  • WebEngine组件中的运行JS:

还是以该槽函数为例:

void Widget::on_pushButtonSatelliteMap_clicked()
{
    QString cmd = "showSatelliteMap()";
    ui->webEngine->page()->runJavaScript(cmd);		// 直接page()下就可以运行
}

在JS单向通信中十分简单,也不需要使用QWebChannel信使,但该方法runJavaScript()无法在构造函数中使用,原因不明。也可以这样使用:

connect(ui->webEngine,&QWebEngineView::loadFinished,[=](int){
             ui->webEngine->page()->runJavaScript(cmd1);

第二个参数SIGNAL位置的,只能使用这样的方式调用,如果使用SIGNAL(....loadFinished),报错。

4. 总结

通过这样的方法,我们就完成了一个初级的过渡。本人由于是研究嵌入式的程序员,只是上位机学习简单的Qt做一点点当成辅助开发,并没有什么高深的Qt技术,也正在学习中,欢迎讨论。

参考文献:

[1] 在水一方著.javascript调用Qt.CSDN博客.2011-07-18.

[2]Я!ńɡ著.QT5利用chromium内核与HTML页面交互.CNBLOGS. 2015-11-17.

[3]liuyez123著.[实现QT与HTML页面通信]. CSDN博客. 2016-01-13.


版权声明:

1. 本文为MULTIBEANS团队研发跟随文章,未经允许不得转载。

2· 文中涉及的内容若有侵权行为,请与本人联系,本人会及时删除。

3· 尊重成果,本文将用的参考文献全部给出,向无私的工程师,爱好者致敬。


** 博文Markdown原版下载:http://pan.baidu.com/s/1qXGljC4 提取码:uk3b **

Qt_开发北斗定位系统融合百度地图API及Qt程序打包发布

Qt开发北斗定位系统融合百度地图API及Qt程序打包发布

1、上位机介绍

最近有个接了一个小型项目,内容很简单,就是解析北斗GPS的串口数据然后输出经纬度,但接过来觉得太简单,就发挥了主观能动性,增加了百度地图API,不但能实时定位,还能在地图上标识出位置信息,用的QT5.5。上位机运行图片如图所示:整体运行比较流畅。

上一个版本的界面

windows版本界面

windows卫星图图片

Linux版本界面

底层设别

原理就是界面上集成一个WebKits/WebView,让Qt和Javascript进行交互。但需要注意Qt5.6以上版本取消了WebView的模块,换成了webenginewidgets,看上去配置好麻烦,甚至还要自己编译什么的,虽然性能可能有指数性的提升,但对于我这个做嵌入式软件和硬件,上位机会个基础的就算是很好的人来说,还是webkits好一点。

2017/07/22 更新:


在本文后续版本已经适配了Qt 5.6 以上版本的QWebEngine版本,摒弃了QWebKits组件,后续若有经费的话,将继续更新支持QWebChannel通信通道。两个版本都可以在本文尾部的附件中下载,欢迎学习讨论。


2. 开发介绍

本设计开发主要涉及三个方面:

  • 串口开发(北斗GPS基于UART的,波特率115200,8,1),这个北斗GPS模块隔1s发一次GPS数据组,会通信几个卫星接收数据,时而一些卫星不反馈数据。
  • 数据解析。数据解析模块包括把几个卫星的数据按协议分开然后解析出来,这里有个难点在于Qt串口和CH340缓存BUG导致的数据包粘连和数据不连续解决。
  • 地图API驱动

2.1 串口开发

串口开发不用说了,请参考我前几篇有个蓝牙的博客,上面有源码,Qt on Android 蓝牙开发,本设计中的串口部分就是基于那个串口开发的。串口开发自动检测连接设备,不需要进入管理器和找到COM口是多少,自动和串口进行连接。

2.2 数据解析

串口数据粘包和数据不连续很头疼,进入一个串口接收槽函数QString rxArray.append(serialPort->readAll() ); 接收数据不完整,或者说会分好几次进行接收,而且分好几次接收长度没有规律,所以无法直接使用接收的数据。

1S钟GPS发送一次数据为:

/*
$GNRMC,114821.880,V,3957.378130,N,11620.848015,E,0.000,0.000,230417,,E,N*23
$GNGGA,114821.880,3957.378130,N,11620.848015,E,0,00,127.000,100.800,M,0,M,,*6D
$GNGLL,3957.378130,N,11620.848015,E,114821.880,V,N*52
$GNGSA,A,1,,,,,,,,,,,,,127.000,127.000,127.000*2A
$GNGSA,A,1,,,,,,,,,,,,,127.000,127.000,127.000*2A
$GPGSV,1,1,4,17,57,315,21,22,35,67,,28,75,176,,30,12,204,*74
*/

数据量比较巨大,所以这里增加处理机制,尽量保存完整数据。

// 接收数据槽函数

void Widget::RxData(){


    QString rxString;

    rxArray.append(serialPort->readAll());
    //qDebug() << QString(rxArray);
    if( serialRead == true ){
        // 数据对齐,如果上次数据是一半,抛弃数据,重新接受
        times++;
        rxString = QString(rxArray);
        //qDebug() << "rec:" << rxString;
        ui->textBrowser->append(tr("--------------------------------------------------------------------------"));
        ui->textBrowser->append("从北斗GPS传感器第("+QString::number(times)+")次接受数据:");
        ui->textBrowser->append(tr("--------------------------------------------------------------------------"));
        ui->textBrowser->append(QString(rxArray));
        gpsDatasProcessing( rxArray );
        rxArray.clear();
        serialRead = false;
        if( times%50 == 0 ) {
            ui->textBrowser->clear();
        }
        ui->textBrowser->append(tr("--------------------------------------------------------------------------\r"));
    }else{
        return;
    }
    // 解析数据

}
void Widget::gpsDatasProcessing(QByteArray GPSBuffer)
{

    QString GPSBufferString = QString( GPSBuffer );
    int error_pos = 0;
    QString GNRMC_String = NULL;
    QString GPGGA_String = NULL;
    QString GPGSV_String = NULL;
    QString GPRMC_String = NULL;
    QString GPGLL_String = NULL;
    QString GNGGA_String = NULL;
    bool latiflag = false;
    bool atiflag = false;
    bool utcflag = false;
    bool speedflag = false;
    bool longtiflag = false;

    QList<QString> gpsStringList = GPSBufferString.split('\n');


    // 由于定时间隔,数据包发生黏连,纠正数据。
    if( gpsStringList.at(0).at(0) != '$' ) {
        QString ErrorString =  gpsStringList.at(gpsStringList.length()-1) + gpsStringList.at(0);
        error_pos = 1;
        if( ErrorString.contains("$GNRMC") ){
            GNRMC_String = ErrorString;
        }else if( ErrorString.contains("$GPGGA") ) {
            GPGGA_String = ErrorString;
        }else if( ErrorString.contains("$GPGSV")  ) {
            GPGSV_String = ErrorString;
        }else if( ErrorString.contains("$GPRMC") ) {
            GPRMC_String = ErrorString;
        }else if( ErrorString.contains("$GPGLL") ) {
            GPGLL_String = ErrorString;
        }else if( ErrorString.contains("$GNGGA") ) {
            GNGGA_String = ErrorString;
        }

    }else{
        error_pos = 0;
    }
    // 从QList中得到数据
    for( int i = error_pos; i < gpsStringList.length()- error_pos; i++ ) {
        if( gpsStringList.at(i).contains("$GNRMC") ){
            GNRMC_String = gpsStringList.at(i);
        }else if( gpsStringList.at(i).contains("$GPGGA") ) {
            GPGGA_String = gpsStringList.at(i);
        }else if( gpsStringList.at(i).contains("$GPGSV")  ) {
            GPGSV_String = gpsStringList.at(i);
        }else if( gpsStringList.at(i).contains("$GPRMC") ) {
            GPRMC_String = gpsStringList.at(i);
        }else if( gpsStringList.at(i).contains("$GPGLL") ) {
            GPGLL_String = gpsStringList.at(i);
        }else if( gpsStringList.at(i).contains("$GNGGA") ) {
            GNGGA_String = gpsStringList.at(i);
        }
    }
    if( !GPGGA_String.isNull() ) {
        QList<QString> gpggaStrList = GPGGA_String.split(",");
        QString utcstr = gpggaStrList.at(1);
        ui->lineEdit_UTC->setText("格林威治时间:"+utcstr.mid(0,2)+":"+utcstr.mid(2,2)+":"+utcstr.mid(4,2));
        QString latistr = gpggaStrList.at(2);
        ui->lineEdit_latitude->setText("北纬"+latistr.mid(0,2)+""+latistr.mid(2,7)+"");
        QString altistr = gpggaStrList.at(4);
        ui->lineEdit_longitude->setText("西经"+altistr.mid(0,3)+""+altistr.mid(3,7)+"");
        utcflag = true;
        latiflag = true;
        atiflag = true;
    }
    if( !GNGGA_String.isNull() ) {
        if( !latiflag ) {
            QList<QString> gnggaStrList = GNGGA_String.split(",");
            QString utcstr = gnggaStrList.at(1);
            UTC2BTC(&utcstr);
            ui->lineEdit_UTC->setText("北京时间:"+utcstr.mid(0,2)+":"+utcstr.mid(2,2)+":"+utcstr.mid(4,2));
            QString latistr = gnggaStrList.at(2);
            ui->lineEdit_latitude->setText("北纬"+latistr.mid(0,2)+"°"+latistr.mid(2,9)+"'");
            double double_lati = latistr.mid(0,2).toDouble()+(latistr.mid(2,7).toDouble()+0.25)/60;
            QString altistr = gnggaStrList.at(4);
            ui->lineEdit_longitude->setText("西经"+altistr.mid(0,3)+"°"+altistr.mid(3,9)+"'");
            double double_alti = altistr.mid(0,3).toDouble()+(altistr.mid(3,7).toDouble()+0.25)/60;

            setCoordinate(QString::number(double_alti),QString::number(double_lati));
            //setCoordinate(QString::number(108.886119),QString::number(34.223921));
            qDebug()<< "纬度:"<<QString::number(double_alti)<<"|"<<"经度:"<< QString::number(double_lati) << "\n";
            QString longtistr = gnggaStrList.at(9);
            ui->lineEdit_altitude->setText(longtistr+"m ");
            ui->lineEdit_speed->setText("无效PPS");

            utcflag = true;
            latiflag = true;
            atiflag = true;

        }
    }

}
void Widget::slotSerialTimerOut()
{
    if( serialRead == false ){
        serialRead = true;
    }
}

2.3 百度地图API

百度地图API我找了很多资料,参考资料本文附录,非常感谢博客名“灿哥哥”,“我是大坏蛋”的整理,“灿哥哥”在文章中不但提供了离线地图和方法,还提供了对于地图的基本介绍,对于地图上面的操作,请参考灿哥哥的博客。但就我开发我想提出两点:

  • 本设计使用的是离线地图,地图包30M左右,不联网也可以使用。把地图包,放在编译的release或者debug文件夹下,载Qt主程序中的url协商地图html的位置。
  • 如果使用的是在线地图就需要连接网络,最重要的是坐标转换**,经纬度需要和百度地图坐标进行一个转换,这个转换是通过百度地图API的接口进行的**,而且普通用户转换还有次数限制。  
  • 这个离线地图不需要转换,直接使用经纬度就可以定位。

下面代码可以看到Qt和Javascript如何互动的。

void Widget::getCoordinate(QString lon,QString lat)
{
    QString tempLon="鼠标经度:"+lon+"°";
    QString tempLat="鼠标纬度:"+lat+"°";
    ui->labelMouseLongitude->setText(tempLon);
    ui->labelMouseLatitude->setText(tempLat);
}

void Widget::setCoordinate(QString lon,QString lat)
{
    QWebFrame *webFrame = ui->webView->page()->mainFrame();
    QString cmd = QString("showAddress(\"%1\",\"%2\")").arg(lon).arg(lat);
    webFrame->evaluateJavaScript(cmd);
}
void Widget::on_pushButtonStreetMap_clicked()
{
    QWebFrame *frame = ui->webView->page()->mainFrame();
    QString cmd = QString("showStreetMap()");
    frame->evaluateJavaScript(cmd);
    ui->pushButtonSatelliteMap->setEnabled(true);
    ui->pushButtonStreetMap->setEnabled(false);
}

void Widget::on_pushButtonSatelliteMap_clicked()
{
    QWebFrame *frame = ui->webView->page()->mainFrame();
    QString cmd = QString("showSatelliteMap()");
    frame->evaluateJavaScript(cmd);
    ui->pushButtonSatelliteMap->setEnabled(false);
    ui->pushButtonStreetMap->setEnabled(true);
}

void Widget::slotPopulateJavaScriptWindowObject()
{
    ui->webView->page()->mainFrame()->addToJavaScriptWindowObject("ReinforcePC", this);

}

3. 程序打包

做完这个程序之后呢,我开始研究如何给程序打包,终于在两个小时内搞定了。

Step1:把所需要的dll文件集成出来。

Step2:用工具把dll文件exe文件和其他文件封装起来,做成msi或者exe文件。

工具就是一个Qt自带的windeployqt工具,另一个是Advanced installer安装包打包程序。

3.1 搜集dll文件

1)在开始菜单找到Qt文件夹,里面有个像是cmd命令行一样的东西,我的是MinGW的。反正运行出来这个样子:

搜集dll的cmd

2)进入Qt的工程文件夹,在release或者debug里面(看你用的是release编译还是debug编译了),找到生成的exe文件夹,把这个exe文件复制到一个方便找,方便输入路径的地方。我放在了D:\setup文件里了。

3)在刚才出那个命令行里面输入: cd /d D:\setup 切换到这个文件夹。

4)输入命令: windeployqt xxx.exe xxx.exe就是你刚才编译出exe的名字。

然后你会发现Qt把所有这个exe文件需要的.dll文件和其他支持库文件都放在这个文件夹了。实际上到了这步你可以打成压缩包然后发布到任何电脑,解压直接运行

3.2 使用Advanced Installer打包程序

我还是比较喜欢把他弄成安装包,这样更方便,更正式。Advanced Installer这个工具简直太赞了,打包的程序安装界面十分的正式,一点都不山寨,还有很多安装包的皮肤可供选择。如图为打好包的图标:

打包后的程序

运行后的效果如图:

img

里面还提供了导入注册表、安装后创建快捷方式等等方便的配置。

** 下载地址:http://down7.pc6.com/gm1/Advanced%20Installer.zip **

使用教程,还是参考后面的参考文献中的【4】,这里不在赘述了。

本文附件:

[1] 本程序旧版安装包下载地址如下百度云盘地址 提取码:41hx (2017-04-29)

[2] 本程序新版安装包下载地址如下:百度云盘地址 提取码:o59r (2017-07-22更新)

参考文献:

[1] 我是大坏蛋,gps定位Qt界面百度地图api的介绍,CSDN博客,2014-08-24
[2] 灿哥哥,Qt加载百度离线地图,CSDN博客,2016-03-30
[3] winland0704,Qt官方开发环境生成的exe发布方式--使用windeployqt,Qt百度贴吧,2015-04-28
[4] Prodesire,Windows安装包制作指南-Advanced Installer的使用,cnBlogs博客,2016-08-18
[5] jwq2011的专栏,GPS数据包格式+数据解析,CSDN博客,2016-12-15

版权声明:

1· 本文为MULTIBEANS团队研发跟随文章,未经允许不得转载。

2· 文中涉及的内容若有侵权行为,请与本人联系,本人会及时删除。

3· 尊重成果,本文将用的参考文献全部给出,向无私的工程师,爱好者致敬。


Linux进程之间的通信-信号量(System V)

Linux进程之间的通信-信号量(System V and POSIX IPC)

  • 信号量 (sem) : 管理资源的访问
  • 共享内存 (shm): 高效的数据分享
  • 消息队列 (msg):在进程之间简易的传数据的方法
  • 互斥算法(Dekker, Peterson, Filter, Szymanski, Lamport面包店算法)

IPC(Inter-Process Communication,进程间通讯)包含三种通信方式,信号量、共享内存和消息队列。在linux编程里面可以有两个不同的标准,一个是SYSTEM-V标准,一个是POSIX标准。以下是两个标准之间的区别1。简单的说,POSIX更轻量,常面向于线程;SYSTEM-V更重一些,需要深陷Linux内核之中,面向于进程。

1. POSIX和SYSTEM-V的区别

Following table lists the differences between System V IPC and POSIX IPC2.

SYSTEM V POSIX
AT & T introduced (1983) three new forms of IPC facilities namely message queues, shared memory, and semaphores. Portable Operating System Interface standards specified by IEEE to define application programming interface (API). POSIX covers all the three forms of IPC
SYSTEM V IPC covers all the IPC mechanisms viz., pipes, named pipes, message queues, signals, semaphores, and shared memory. It also covers socket and Unix Domain sockets. Almost all the basic concepts are the same as System V. It only differs with the interface
Shared Memory Interface Calls shmget(), shmat(), shmdt(), shmctl() Shared Memory Interface Calls shm_open(), mmap(), shm_unlink()
Message Queue Interface Calls msgget(), msgsnd(), msgrcv(), msgctl() Message Queue Interface Calls mq_open(), mq_send(), mq_receive(), mq_unlink()
Semaphore Interface Calls semget(), semop(), semctl() Semaphore Interface Calls Named Semaphores sem_open(), sem_close(), sem_unlink(), sem_post(), sem_wait(), sem_trywait(), sem_timedwait(), sem_getvalue() Unnamed or Memory based semaphores sem_init(), sem_post(), sem_wait(), sem_getvalue(),sem_destroy()
Uses keys and identifiers to identify the IPC objects. Uses names and file descriptors to identify IPC objects
NA POSIX Message Queues can be monitored using select(), poll() and epoll APIs
Offers msgctl() call Provides functions (mq_getattr() and mq_setattr()) either to access or set attributes 11. IPC - System V & POSIX
NA Multi-thread safe. Covers thread synchronization functions such as mutex locks, conditional variables, read-write locks, etc.
NA Offers few notification features for message queues (such as mq_notify())
Requires system calls such as shmctl(), commands (ipcs, ipcrm) to perform status/control operations. Shared memory objects can be examined and manipulated using system calls such as fstat(), fchmod()
The size of a System V shared memory segment is fixed at the time of creation (via shmget()) We can use ftruncate() to adjust the size of the underlying object, and then re-create the mapping using munmap() and mmap() (or the Linux-specific mremap())

我从文献里面得到几个进程持续性概念,我觉得这个角度分类比较好。直接抄文献:从IPC的持续性角度而言,可以把进程通信分为以下几类3

  • 随进程持续 (Process-Persistent IPC)

  • IPC对象一直存在,直到最后拥有他的进程被关闭为止,典型的IPC有pipes(管道)和FIFOs(先进先出对象)

  • Pipe, FIFO, Posix的mutex(互斥锁), condition variable(条件变量), read-write lock(读写锁),memory-based semaphore(基于内存的信号量) 以及 fcntl record lock,TCP和UDP套接字,Unix domain socket

  • 随内核持续 (Kernel-Persistent IPC)

  • IPC对象一直存在直到内核被重启或者对象被显式关闭为止,在Unix中这种对象有System V 消息队列,信号量,共享内存。(注意Posix消息队列,信号量和共享内存被要求为至少是内核持续的,但是也有可能是文件持续的,这样看系统的具体实现)。

  • Posix的message queue(消息队列), named semaphore(命名信号量), System V Message queue, semaphore, shared memory。

  • 随文件系统持续 (FileSystem-Persistent IPC)

  • 除非IPC对象被显式删除,否则IPC对象会一直保持(即使内核才重启了也是会留着的)。如果Posix消息队列,信号量,和共享内存都是用内存映射文件的方法,那么这些IPC都有着这样的属性。

  • 要注意的是,虽然上面所列的IPC并没有随文件系统的,但是我们就像我们刚才所说的那样,Posix IPC可能会跟着系统具体实现而不同(具有不同的持续性),举个例子,写入文件肯定是一个文件系统持续性的操作,但是通常来说IPC不会这样实现。很少有IPC会实现文件系统持续,因为这会降低性能,不符合IPC的设计初衷。

System V IPC不是随进程持续的,是随内核持续的。

上面是摘录的,下面谈下我的理解:我们在Linux编程里面,关于线程可以使用pthread_mutex, spinlock这些工具,这些工具都是在一个进程中的,守护的是进程内部的资源,因此作者提到随进程持续的概念;而两个无关进程之间对于访问同一个资源,比如文件,也是可能会有临界区,只是相比于进程内部的临界区,扩展到了系统内部的临界区,因此这里有随内核持续的概念。这也是为什么POSIX是一个轻量级的常用于线程的,而System V IPC是一个深陷内核的常用于进程的标准。我相信IPC在进程层级和线程层级既有相似的点也有不同的点。

2. 信号量

Linux-用户空间-多线程与同步4中,引用了sem_xxx()的接口,根据上面的信息我们也可以知道这是POSIX IPC的接口。我们在那个文章中并没有阐述信号量和spinlock的区别,在网上大多数人只谈论到信号量和锁之间的用法上的区别,或者是意义上的区别。我这里想更进一步的解释信号量的实现和锁是有差别的。还有pthread_xxx里面的spinlock和内核的spinlock理念是一样的,但是调度完全不一样,pthread_xxx的可以关注这个实验4

  • 互斥锁用于互斥(弹走另一个线程),信号量用于同步(不同的level,不同的同步)5,pthread用户接口并没有提供信号量的相关接口
  • 独占访问1,可以使用Dekker算法,但是算法依赖于spinlock忙等待,极大的耗费CPU资源(传统设备)。
  • 独占访问2,高级一点硬件上面支持独占访问(ARMv8独占指令LDXR/STXR,ARM的独占监视器机制)6,可以原子性的增加寄存器的值,节约了CPU的资源7
  • spin_lock底层实现在ARM架构上面使用了内存独占的LDXR和STXR指令,也使用了WFE指令,让ARM进入到低功耗状态57, 而x86架构是PAUSE指令。
  • 在信号量内也使用了spin_lock,但是是raw_spinlock (arch_spin_lock)58。raw_spinlock禁止内核抢占,然后调用spin_lock。

System V中提供的信号量接口更通用相比于POSIX标准的信号量9。我们从后面的API介绍上面可以看出,system V中的提供的信号量接口是非常复杂和繁琐的,虽然接口少,但是标识符,标志位特别多,而且里面定义了很多自己的结构体的结构,甚至出现了变长参数。根据文献9,我们大分部使用的都是二进制的信号量,但我相信,这些接口都不是白给的,肯定有更多的使用场景,可能需要以后工作情景上面进行挖掘了。

2.1 APIs

2.1.1 semctl10

semctl() performs the control operation specified by cmd on the System V semaphore set identified by semid, or on the semnum-th semaphore of that set. (The semaphores in a set are numbered starting at 0.)

#include <sys/sem.h>

int semctl(int semid, int semnum, int cmd, ...);

Parameters:

Params I/O Details
int semid Input semid是由semget接口返回的标识符
int semnum Input sem_num参数是信号量的编号,当需要用到成组的信号量时候,就需要合格参数,一般取值为0,表示这是一个也是唯一一个信号量。
int cmd Input 将要采取动作,参考宏定义SETVAL, IPC_RMID, IPC_SET
... Input 为一个union结构体,根据X/OPEN规范的定义包含 val, *buf, *array

Return:

根据cmd不同,返回值也不同。对于 SETVAL和IPC_RMID而言,成功时返回0,失败时返回-1。

2.1.2 semget11

创建一个新的信号量或取得一个已有的信号量的key。

The semget() system call returns the System V semaphore set identifier associated with the argument key. It may be used either to obtain the identifier of a previously created semaphore set (when semflg is zero and key does not have the value IPC_PRIVATE), or to create a new set.

A new set of nsems semaphores is created if key has the value IPC_PRIVATE or if no existing semaphore set is associated with key and IPC_CREAT is specified in semflg.

If semflg specifies both IPC_CREAT and IPC_EXCL and a semaphore set already exists for key, then semget() fails with errno set to EEXIST. (This is analogous to the effect of the combination O_CREAT | O_EXCL for open(2).)

#include <sys/sem.h>

int semget(key_t key, int nsems, int semflg);

Parameters:

Params I/O Details
key_t key Input 不相关的进程可以通过它访问同一个信号量。程序对所有的信号量访问都是间接的,先提供个key,接着系统生成一个对应的信号量标识符,只有semget函数直接使用key,其他的函数都适用semget返回的值。
int nsems Input 指定需要的信号量数目,它几乎总是1
int semflg Input 一组标识,和open类似,低9bit该信号的权限。可以通过联合使用IPC_CREAT和IPC_EXCL来确保创建一个新的,唯一的信号量。如果该信号量已经存在,则返回一个错误。

Return:

返回非零整数,为semctl,semop的标识符,返回<0失败。

2.1.3 semop12

改变信号量的值。

semop() performs operations on selected semaphores in the set indicated by semid. Each of the nsops elements in the array pointed to by sops is a structure that specifies an operation to be performed on a single semaphore. The elements of this structure are of type struct sembuf, containing the following members:

#include <sys/sem.h>

struct sembuf {
  unsigned short sem_num;  /* semaphore number */
	short          sem_op;   /* semaphore operation */
	short          sem_flg;  /* operation flags */
};

int semop(int semid, struct sembuf *sops, size_t nsops);
int semtimedop(int semid, struct sembuf *sops, size_t nsops,
               const struct timespec *timeout);    // _GNU_SOURCE

Parameters:

Params I/O Details
nt semid Input semid是由semget接口返回的标识符
struct sembuf* sops Input sem_num:信号量编号,一般取值为0,除非使用一组信号;sem_op:一次操作中需要改变的数值,+1为V操作,-1为P操作;sem_flg:通常被设置为SEM_UNDO,将使得操作系统跟踪当前进程对这个信号量的使用情况,如果一个进程没有释放该信号量终止,操作系统将自动释放进程持有的信号量。如果对信号量没有特殊要求,记得将sem_flg设置为SEM_UNDO,如果决定使用一个非SEM_UNDO的值,那就一定要注意保持设置的一致性,否则会搞不清楚内核在进程退出的时候是否清理信号。
size_t nsops Input nsops elements in the array pointed to by sops is a structure that specifies an operation to be performed on a single semaphore.

Return:

On successful completion, the sempid value for each semaphore specified in the array pointed to by sops is set to the caller's process ID.

2.2 Example

根据文献9提供的示例,完成使用System V级别的信号量,创建简单的二进制信号量,满足以下条件:

  • 创建一个进程,允许该进程多次reentrant,塑造临界区竞争的场景。
  • 在系统上创建一个文件common.txt,一个分行写入10x10的1, 一个分行写入10x10的0,通过输入参数实现。
  • 写入操作使用信号量进行保护。
  • 最后打开文件,需要看见1矩阵不会被破坏,0矩阵也不会被破坏。

关于semun.h请参考https://man7.org/tlpi/code/online/dist/svsem/semun.h.html, MACOS里面自带了定义,但是在Ubuntu和ARM-Linux上面并没有这个定义。

这里还要说一下,因为是使用的系统上的资源竞争(同一个文件),因此必须使用System V级别的信号量才能完成,而posix提供的同步的信号量的内存都是存在一个进程里面的。

这个例子划分 写入文件十次为一个临界区。

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <sys/file.h>
#include <sys/sem.h>

#define debug_log printf("%s:%d--", __FUNCTION__, __LINE__);printf

#if defined(__linux__)
union semun {                   /* Used in calls to semctl() */
    int                 val;
    struct semid_ds *   buf;
    unsigned short *    array;
#if defined(__linux__)
    struct seminfo *    __buf;
#endif
};
#endif

static int set_semvalue(int sem_id)
{
    int ret = 0;
    union semun su;
    su.val = 1;
    ret = semctl(sem_id, 0, SETVAL, su);
    if (ret != 0) {
        debug_log("failed on semctl, ret = %d\n", ret);
        goto finish;
    }
finish:
    return ret;
}

static void del_semvalue(int sem_id)
{
    int ret = 0;
    union semun su;
    ret = semctl(sem_id, 0, IPC_RMID, su);
    if (ret != 0) {
        debug_log("failed on semctl, ret = %d\n", ret);
    }
}

static int sem_p(int sem_id)
{
    struct sembuf sem_b;
    int ret = 0;

    sem_b.sem_num = 0;
    sem_b.sem_op = -1;  /* P() */
    sem_b.sem_flg = SEM_UNDO;
    ret = semop(sem_id, &sem_b, 1);
    if (ret != 0) {
        debug_log("semop failed, ret = %d\n", ret);
    }
    return ret;
}

static int sem_v(int sem_id)
{
    struct sembuf sem_b;
    int ret = 0;

    sem_b.sem_num = 0;
    sem_b.sem_op = 1;  /* V() */
    sem_b.sem_flg = SEM_UNDO;
    ret = semop(sem_id, &sem_b, 1);
    if (ret != 0) {
        debug_log("semop failed, ret = %d\n", ret);
    }
    return ret;
}

int main(int argc, char *argv[])
{
    int i, ret;
    char op_chars[20];
    FILE *fd = NULL;
    int count = 0;
    int sem_id = 0;

    fd = fopen("common.txt", "a+");
    if (NULL == fd) {
        debug_log("failed on fopen\n");
        goto finish1;
    }
    sem_id = semget((key_t)1234, 1, 0666|IPC_CREAT);

    if (sem_id < 0) {
        debug_log("failed on semget\n");
        goto finish2;
    }

    if (argc > 1) {
        strcpy(op_chars, "111111111\n");
        debug_log("op char will write 1 to common.txt\n");
        ret = set_semvalue(sem_id);
        if (ret != 0) {
            debug_log("set_semvalue failed\n");
            goto finish2;
        }
    } else {
        strcpy(op_chars, "000000000\n");
        debug_log("op char will write 0 to common.txt\n");
    }

    debug_log("start write file.....\n");
    debug_log("sem_p().....\n");
    ret = sem_p(sem_id);
    if (ret != 0) {
        debug_log("failed on sem_p\n");
        goto finish3;
    }
    for (i = 0; i < 10; i++) {
        ret = fwrite(op_chars, 1, strlen(op_chars), fd);
        if (ret < 0) {
            debug_log("failed on fwrite\n");
            goto finish3;
        }
        count += ret;
        sleep(1);
        debug_log("write 10 bytes to file. total = %d\n", count);
    }
    debug_log("sem_v().....\n");
    ret = sem_v(sem_id);
    if (ret != 0) {
        debug_log("failed on sem_v\n");
    }

finish3:
    if (argc > 1) {
        // it is very important for another process using the semvalue.
        debug_log("hold process .... 20s\n");
        sleep(20);
        del_semvalue(sem_id);
    }
finish2:
    fclose(fd);
finish1:
    debug_log("finish test...\n");
    return ret;
}

$ rm -rf common.txt

$ ./test_sem.elf 1

$ ./test_sem.elf

image-20220401100544018

Reference

Footnotes

  1. System V IPC vs POSIX IPC - Stack Overflow

  2. System V & Posix (tutorialspoint.com)

  3. UNIX 进程间通讯(IPC)概念(Posix,System V IPC)

  4. Linux-用户空间-多线程与同步 2

  5. 一文搞懂 | Linux 同步管理(上) 2 3

  6. 06_ARMv8_指令集_一些重要的指令 · Issue #12 · carloscn/blog (github.com)

  7. ARM WFI和WFE指令 2

  8. 自旋锁spin_lock和raw_spin_lock

  9. Linux系统编程(第四版)- page490 2 3

  10. archlinux man page - semctl

  11. archlinux man page - semget

  12. archlinux man page - semop

基于Mac Silicon M1 的OpenSSL 编译

Building OpenSSL for ARM/Apple silicon Macs

JUNE 22, 2020

I tend to shy away from dependencies when possible. But my app, Aether, has an unavoidable dependency on tqsllib, which in turn depends on OpenSSL. Originally, OpenSSL shipped with macOS, so using it was no big deal. Apple deprecated it years ago (for very good reasons) and recommends building it yourself from up-to-date source if you need it. So, I do just that.

Normally, building OpenSSL is pretty straightforward. Download the source, run:

./configure darwin64-x86_64-cc
make
make install

and you’re done.

As of right now (June 22, 2020), if you want to build it for Apple’s newly announced Macs with "Apple silicon” (aka ARM), it doesn’t work out of the box. I managed to get it to build and thought I’d share what I did. I did this on an Intel Mac running the Xcode 12.0 for macOS Universal Apps beta, but I believe these instructions should work without changes on an ARM Mac as well.  Disclaimer: This is a bit of a hack job. I expect that the OpenSSL project will add official support fairly quickly so this stuff won’t be necessary. I may try to submit a patch, but I’m not confident enough of the details here to say that it’s the best way to do things or without any lurking issues. So, as always, use this at your own risk! I’m not an SSL expert, a security expert, nor a build system expert.

Here’s how I built OpenSSL for ARM Macs:

1.Install "Xcode 12 for macOS Universal Apps beta"

2.Switch to using the beta for command line tools using:

sudo xcode-select -s /Applications/Xcode-beta.app

3.Download the OpenSSL source code:

git clone git://git.openssl.org/openssl.git

4.Switch to the openssl directory:

cd openssl

Optional 4b. I need OpenSSL 1.1.1, so I switched to that branch:

git switch OpenSSL_1_1_1-stable

5.Open the file Configurations/10-main.conf in your favorite text editor:

bbedit Configurations/10-main.conf

6.OpenSSL uses its own configuration system, and it uses this to configure certain things when building for a particular target. Most importantly, we need to specify the correct architecture (arm64) for Apple silicon/ARM Macs. To do this, find the section where the configuration for darwin64-x86_64-cc is defined. It’s line 1552 as I write this. Add the following new configuration for ARM Macs under it:

"darwin64-arm64-cc" => {
    inherit_from     => [ "darwin-common", asm("aarch64_asm") ],
    CFLAGS           => add("-Wall"),
    cflags           => add("-arch arm64"),
    lib_cppflags     => add("-DL_ENDIAN"),
    bn_ops           => "SIXTY_FOUR_BIT_LONG",
    perlasm_scheme   => "macosx",
},

7.Build for ARM by running the configure script, followed by make and make install. This will build for arm64, and place the results in /tmp/openssl-arm

./Configure darwin64-arm64-cc --prefix="/tmp/openssl-arm" no-asm
make
make install

The no-asm option tells the build system to avoid using assembly language routines, instead falling back to C routines. This prevents errors during build on ARM. The same option is not necessary for the x86 build. I’m sure there’s a way to fix building the ASM routines for arm64 (they build fine for iOS), but it’s beyond me right now.

8.Build for x86_64 by running the build steps again again like so:

./Configure darwin64-x86_64-cc --prefix="/tmp/openssl-x86"
make
make install

9.Lipo the results together to create a single, universal static library:

lipo /tmp/openssl-arm/lib/libssl.a /tmp/openssl-x86/lib/libssl.a -create -output libopenssl/lib/libssl.a
lipo /tmp/openssl-arm/lib/libcrypto.a /tmp/openssl-x86/lib/libcrypto.a -create -output libopenssl/lib/libcrypto.a

10.You’ll also need the headers. I just copy them like so:

cp /tmp/openssl-arm/include/openssl/* include/openssl/

11.If everything worked, you’ll now have static library binaries for libcrypto and libssl in lib/, along with headers in include/openssl/

DSP+ARM多核异构开发环境搭建OMAPL138

DSP+ARM多核异构开发环境搭建OMAPL138

注意: 环境为Ubuntu 12.04 只能是这个环境。我甚至在Ubuntu16.04上面安装了VMware,然后,在装了一个Ubuntu 12.04 x86版本。

导语与感想

OMAPL138属于多核异构平台(DSP+ARM),多核通信是多核异构平台的精髓部分,目前市面上流行的还有ZYNQ平台(FPGA+ARM),同样通信机理复杂。德州仪器OMAPL138和Davinci使用一样的多核通信机理。

这个机制相当复杂,又要懂Linux,又要会调试DSP,又要熟练掌握ARM的嵌入式Linux,又要抓住多核通信机制,实在让人抓狂。DSP端使用CCS,用JTAG口进行仿真,ARM端使用终端GDB命令进行动态调试配合调试输出完成多核通信的开发。

好吧,步入正题了,准备好以下的素材(不要被吓到)

**一定要对于文件、编译器有个很好的管理,杜绝东一块西一块,左一个文件,又一个文件,否则到时候自己蒙了。**本人习惯在自己用户的文件夹创建opt文件夹(用于安装非root权限运行的软件),script文件夹(用于处理一些脚本文件)、work(待交叉编译的源代码文件)、setup(安装、压缩包文件)、workspace(编程工程文件路径)、lib(第三方库文件夹)

搭建前准备素材

  • CGT_组件:ti_cgt_c6000_7.3.0.tar.gz
  • 多核通信组件:mcsdk_1_01_00_02_setuplinux.bin
  • C6000的Starterware库:OMAPL138_StarterWare_1_10_04_01-Linux-x86-Install
  • Qt图形界面库:qwt-6.1.0.tar.bz2
  • DSP BIOS组件:bios_5_41_10_36.tar.gz
  • dsplink组件:dsplink_linux_1_65_00_03.tar.gz
  • Qt源文件:qt-everywhere-opensource-src-4.8.3.tar.gz
  • DSP编译工具链:xdctools_3_22_01_21.tar.gz
  • CCS的Linux版本:ccs 5.5 for Linux
  • 内核源文件:linux-3.3.tar.bz2
  • arm-linux交叉编译工具链:arm-2009q1-161-arm-none-eabi.bin

以上这些文件,全部存在~/setup文件下

环境前提

  1. Ubuntu版本为12.04,(不要尝试新版,OMAPL停更了,组件最新支持到ubuntu12.04)
  2. 配置好交叉编译环境
  3. Linux3.3内核编译正确
  4. Qte编译正确
  5. CCS的Linux版本安装好

编译Linux3.3内核

参考我的博客:(基于OMAPL:Linux3.3内核的编译)[https://www.cnblogs.com/sigma0/p/9149041.html]

编译正确Qt

Qt版本使用的是Qt4,Qt5还没有实验,等着实验完Qt5会过来更新。

参考我的博客:(Linux编译Qt4的环境_OMAPL138
)[https://www.cnblogs.com/sigma0/p/8168313.html]

最后我之前设定的Qt make install的路径室/opt/qt-arm-4.8.3 (后面会用到)

编译QWT组件

qwt 全称是"Qt Widgets for Technical Applications",是一个基于 LGPL 版权协议的开源
项目,可生成各种统计图。QWT的编译需要基于上一章节编译QT,编译出的qmake编译器

解压qwt

准备qwt-6.1.0.tar.bz2文件,解压到~/work路径下:
tar -xvf qwt-6.1.0.tar.bz2 -C ~/work

配置QWT编译环境(使用创龙公司)

需要修改两个地方:

  • 在"qwt-6.1.0/qwtconfig.pri"文件第 100 行 QwtOpenGL 和 119 行 QwtDesigner 前面增加。符号"#",表示注释掉此两行,因为此例程没有使用 QwtOpenGL 和 QwtDesigner。如下图所示:
  • 修改 QWT_INSTALL_PREFIX 最后QWT输出路径。
    注释掉的信息
    修改输出路径

在 qwt 目录下执行以下命令产生 Makefile 编译文件:
/opt/qt-arm-4.8.3/bin/qmake

ls 如果有Makefile文件则表示配置成功。

编译QWT和安装

在 qwt 根目录下执行以下命令编译 qwt 组件源码:
make -j4 启动4个线程编译

编译完成后:

在 qwt 根目录下执行以下命令安装 qwt 组件:
sudo make install

该组件会解压到QWT_INSTALL_PREFIX指定的路径中:/opt/qwt-6.1.0

将库发送到开发板(HOST)端

将"/opt/qwt-6.1.0/lib"下的所有文件拷贝到开发板文件系统"/usr/lib"目录下,用SD卡也可以,用scp命令也可以。

安装CCS

安装过程请参考我发在贴吧上的教程:我这里用的是CCS5.5版本,大同小异 (在LINUX(ubuntu)系统下装CCSv6方法)[https://tieba.baidu.com/p/3698761357]

注意路径安装到 /opt/ti下

安装StarterWare库

执行:sudo ./OMAPL138_StarterWare_1_10_04_01-Linux-x86-Install
安装路径为: /opt/ti下

安装配置MCSDK

MCSDK是多核通信组件。

安装MCSDK

准备好mcsdk_1_01_00_02_setuplinux.bin,注意路径安装到/opt/ti下,完全安装就好。

sudo ./mcsdk_1_01_00_02_setuplinux.bin

配置MCSDK

进入"mcsdk_1_01_00_02"目录下,启动 MCSDK 设置脚本,根据不同主机设置,进行tftp、nfs、U-Boot 等配置。在设置之前,务必保证虚拟机网络畅通。

cd /opt/ti/mcsdk_1_01_00_02/
sudo ./setup.sh

  • 问你NFS目标地址安装在哪里,直接回车
  • 问你是否是root权限启动的配置,直接回车
  • 创建EXEC_DIR等等环境变量,回车
  • 问tftp路径直接回车
  • 串口部分,我们用的室CH340所以是/dev/ttyUSB0
  • 问ip,输入omapl138板子的ip地址
  • 问你Linux Kernal位置,我的在SD卡
  • 问你root file system的位置,我的在SD卡
  • 问nfs文件系统启动方式启动方式,直接回车
  • 启动tftp下载内核镜像,n
  • installing linux devkit Y
  • 最后看到TI SDK SETUP COMPLETED配置已经完成。
     在安装完之后TI路径下就该有这些东西

SYSLINK的配置和安装

cd /opt/ti/syslink_2_21_01_05
进入该路径下,打开配置文件:
sudo vim products.mak
要改的内容在下面

DEVICE	=	OMAPL1XX
SDK	=	NONE
EXEC_DIR	= /media/delvis/rootfs  // host root文件系统路径 可以在SD卡上(需要挂载),也可以暂时存储到你电脑临时文件夹上,到时候拷贝到SD卡上
DEPOT	=	/opt/ti		// MCSDK 安装路径

接下来配置的如图所示:

配置完成后保存退出。

编译syslink源代码

编译 syslink 之前,先将以下两个宏定义添加到 syslink 中的 Omapl1xxIpcInt.c、omapl1xx_phy_shmem.c、omapl1xxpwr.c 文件开头,否则编译会出错。
(1) /opt/ti/syslink_2_21_01_05/packages/ti/syslink/ipc/hlos/knl/notifyDrivers/arch/omapl1xx/Omapl1xxIpcInt.c
(2) /opt/ti/syslink_2_21_01_05/packages/ti/syslink/family/hlos/knl/omapl1xx/omapl1xxdsp/Linux/omapl1xx_phy_shmem.c
(3) /opt/ti/syslink_2_21_01_05/packages/ti/syslink/family/hlos/knl/omapl1xx/omapl1xxdsp/omapl1xxpwr.c

  • Omapl1xxIpcInt.c 文件,修改在头处添加文件
#undef __ASM_ARCH_HARDWARE_H
#include <mach/hardware.h>
  • omapl1xx_phy_shmem.c文件,修改在头文件处添加
#undef __ASM_ARCH_HARDWARE_H
#include <mach/hardware.h>
  • omapl1xxpwr.c 文件,修改在头文件处添加
#undef __ASM_ARCH_HARDWARE_H
#include <mach/hardware.h>

编译syslink

cd /opt/ti/syslink_2_21_01_05
make syslink

编译syslink示例

make samples

安装syslink驱动程序

sudo make install
返回到在setup.sh配置syslink的时候指定的rootfs目录,ls lib/modules/3.3.0/kernel/drivers/dsp/

可以看到在文件系统"lib/modules/3.3.0/kernel/drivers/dsp/"目录下有 syslink 驱动程序syslink.ko 文件和文件系统根目录下有"ex**_##"的示例程序。
就配合环境成功了。

参考文献

[1]创龙公司,基于 OMAPL138 的多核软件开发组件 MCSDK 开发入门
[2]创龙公司,OMAPL138基于SYSLINK的双核例程

Qt_QWebChannel和JS、HTML通信/交互驱动百度地图

Qt的QWebChannel和JS、HTML通信/交互驱动百度地图

0 前言

我一个研究嵌入式的,不知道怎么就迷上了上位机,接了几个项目都是关于Qt,这个项目还是比较经典的,自己没事儿的时候也进行研究,对这个软件进行升级,反正,我喜欢编程,喜欢研究这些东西。研究了一下午,查了很多资料,看了很多的例子,我对于JS是0基础,能稍微看懂一点点HTML语言的东西,下午调试了好几遍,运行了好几遍,终于在我更改了JavaScript的函数完毕之后,一下子数据回传成功了,那种感觉很棒!!!特地写这个博客,记录我的研发成果。

  • 我是一个业余研究Qt的,不是那么专业! *

【正文】

QWebChannel官方给的例子是和Socket混合,看的是一头雾水,不得不吐槽Qt了,能不能单独讲讲使用方法,别和别的东西结合?!

对于我的GPS地图项目,研究又有了新的进展,我在本年度一直在业余时间研究Qt开发的软件和网页交互(JS和HTML),虽然我对于网页的知识仅仅有个基础,在业余方面,我也不断的在对该软件进行升级研究,目前状态:

  • 第一阶段:在Qt5.6以下版本,Qt还没有删除QWebKits组件,基于QWebKits组件,我做了最基本的软件:参考:Qt开发北斗定位系统融合百度地图API及Qt程序打包发布
  • 第二阶段:Qt5.6以上版本,删除了QWebKits组件,升级为QWebEngine组件,但是过渡阶段只能由Qt的C++程序去运行JS,JS反馈的数据无法回传,这造成了我获取当前鼠标经纬度的功能缺失。参考:QtWebkits如何向QtWebEngine过渡
  • 第三阶段:基于第二个阶段,依旧基于QWebEngine,引入QWebChannel通信机制,完成JavaScript给的回传数据,还原了鼠标当前经纬度的获取功能。

到目前为止,GPS定位系统交互驱动百度地图已经完全适配QWebEngine组件。

如图1:经纬度功能展示

红色方框部分为当前鼠标的经纬度信息,这个信息来源于html访问百度地图的JavaScript,然后回传给C++的信息。

本文重点来说一下,如何从JavaScript获取回传信息,实现交互

1 实现过程

1.1 原理阐述

我是非专业的,我也没有找关于HTML和JS交互的书,在我研究的过程中我认为是一个这样的关系:

原理阐述

QWebEngine提供了调用HTML里面JavaScript的方法,这里HTML像是一个接口,在HTML尾部有一个这样的标签,<script> 函数和变量体 </script> ,在这个标签内的函数和变量体中写入一些函数和变量,这些函数和变量要么是JavaScript中的调用,要么是Qt中的调用,所以,HTML像是一个QT和网页的桥梁。在下面的章节,我们详细讨论一下步骤。

1.2 WebChannel的使用

要使用QWebChannel要打点好两个方面,第一,Qt方面,需要包含QWebChannel类,注册好QWebChannel需要连接的Qt的对象;第二,JS方面,官方提供了配套的qwebchannel.js文件,这个js文件就相当于驻JavaScript负责通信的。所需准备的:

  • 【JS方面:】qwebchannel.js文件(Qt官方提供,需要在html中引入,拿到后里面什么代码都不要修改,直接使用。qwebchannel.js下载地址
  • 【Qt方面:】在Qt的主程序中,创建好QWebChannel类

下面就这两方面讨论如何使用:

1.2.1 JS方面处理

地图这块仅有一个JavaScript文件,是驱动百度地图的,但是为了让QWebChannel和百度的JS顺利通信,Qt提供了一个qwebchannel.js文件,这个文件就是负责打点Qt和JS通信用的。

参考源码:我把qwebchannel.js放在和百度地图提供的js(也就是你要通信的JS)放在了一个目录,然后在HTML文件中要引入两个js文件。

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>地图演示</title>
<script src="js/qwebchannel.js"></script>                                <!--> !!!!!!重点1<-->
<script src="js/apiv1.3.min.js"></script>							
<!--script type="text/javascript" src="http://api.map.baidu.com/api?v=1.3"></script-->
<link rel="stylesheet" type="text/css" href="bmap.css"/>
</head>
<body>
<div style="left:0;top:0;width:100%;height:100%;position:absolute;" id="container"></div>
</body>
</html>

<script>

var mapOptions={
                minZoom: 3,
                maxZoom:19,
                mapType:  BMAP_NORMAL_MAP
              }
var map = new BMap.Map("container",mapOptions);      // 设置街道图为底图
var point = new BMap.Point(116.468278, 39.922965);   // 创建点坐标
map.centerAndZoom(point,14);                         // 初始化地图,设置中心点坐标和地图级别。

map.addControl(new BMap.NavigationControl({offset: new BMap.Size(10, 90)}));
map.enableScrollWheelZoom();                  // 启用滚轮放大缩小。
map.enableKeyboard();                         // 启用键盘操作。
map.enableContinuousZoom();                   // 启用连续缩放
var myIcon = new BMap.Icon("images/Point.png", new BMap.Size(20,25));
var marker = new BMap.Marker(point,{icon:myIcon});   // 创建标注
map.addOverlay(marker);                              // 加载标注

// !!!!重点2!!!
new QWebChannel(qt.webChannelTransport,
    function(channel){
        window.bridge = channel.objects.person; // 注册
    }
);

var dragFlag=false;

// !!!!重点3!!!
var updateInfo = function(lng,lat)
{
    window.bridge.getCoordinates(lng,lat);
}

function showAddress(longjitude,latitude)
{
   var gpsPoint = new BMap.Point(longjitude, latitude);

   if(!dragFlag)
   {
     map.panTo(gpsPoint);
   }
   marker.setPosition(gpsPoint);
}

function showStreetMap()
{
   map.setMapType(BMAP_NORMAL_MAP);
};

function showSatelliteMap()
{
   map.setMapType(BMAP_SATELLITE_MAP);
}
// !!!!!重点4!!!
map.addEventListener("mousemove",function(e) {		

    updateInfo(e.point.lng,e.point.lat);

});

map.addEventListener("dragstart",function(e){
   dragFlag=true;

});

map.addEventListener("dragend",function(e){
   dragFlag=false;
});

map.addEventListener("zoomend",function(e){

});


</script>

这么多代码,看的你很焦躁,不用太细研究,里面只是定义了很多百度地图读取的方法,我们把重点放在几个点上(你可以在上面的源码注释中找到重点标记)

  • 【第1步】:<script src="js/qwebchannel.js"></script> <script src="js/apiv1.3.min.js"></script> ,这两个语句表示这个HTML负责驱动两个js文件,一个是百度地图的js文件,一个是qwebchannel的js文件,qwebchannel毋庸置疑就是负责交互数据的了,所以在你拿到百度地图原版的HTML文件的时候,需要对这里进行改进,原理貌似就像是C语言中#include这块,把qwebchannel.js集成进来。
  • 【第2步】:需要在JS这块创建一个QWebChannel,这个js里的webchannel就是负责和Qt C++里面的webchannel通信的。
// !!!!重点2!!!
new QWebChannel(qt.webChannelTransport,							// 新建一个QWebChannel实例化
    function(channel){
        window.bridge = channel.objects.person; // 注册          // window.bridge不用找了,这个是js的功能函数,等号后边需要注意,channel.objects.XXXXX
    }                                                           // 这个XXXX是需要在Qt C++程序里面定义的,我们一会儿说,但是channel.objects.这个是固有的。
);

这部分是新添加的,我查了QWebChannel这个东西在qwebchannel.js文件中定义了,这里在我们要访问的HTML中必须要有这个东西的定义,解释详见注释。person这个东西就是在C++里面定义的,就当是他是负责和我们C++和js通信的句柄吧。将他赋值给window.bridge,以后利用操作window.bridge我们就可以通信了。

  • 【第3步】:利用channel这个东西通信。
// !!!!重点3!!!
var updateInfo = function(lng,lat)
{
    window.bridge.getCoordinates(lng,lat);
}

这里定义一个函数,就是将lng和lat这两个参数传递给Qt C++,通过这样的方式就能回传数据了。window.bridge说过了是固有的,js固有的,那么getCoordinates这个东西是什么?答曰,是我们Qt C++里面自定义的一个槽函数,声明在public slots:里面,channel通过window.bridget来操控槽函数,达到数据回传,这个地方是重中之重!!

  • 【第4步】:配合监听器,这里有个注意点。
// !!!!!重点4!!!
map.addEventListener("mousemove",function(e) {		

    updateInfo(e.point.lng,e.point.lat);

});

当发生mousemove这个行为的时候,这可能是JS里面的知识,则调用function(e),一定要注意这个function不能直接把函数体写在这里,必须采用我们上面的方法,把函数体单独写,然后在内部写上调用。

到此javascript部分已经交代清楚了。

1.2.2 Qt C++方面处理

Qt C++方面的开发,需要启动QWebChannel类,注册页面。

    // 准备Javascript文件交互
    QString strMapPath = "qrc:/map/map.html";                   // 设定地图路径

    QWebEnginePage *page = new QWebEnginePage(this);            // 定义QWebEnginePage界面负责打开html文件
    QWebChannel *channel = new QWebChannel(this);               // 定义QWebChannel负责

    channel->registerObject(QString("person"),this);            //  QWebChannel对Widget类,注册一个person的通信介质 /
                                                                //  在js文件中person负责成为window.bridge /
                                                                //  在this中也就是Widget类注册channel,channel访问的位Widget下的槽函数。
    page->load(strMapPath);                                     //  webenginePage加载html地图。
    page->setWebChannel(channel);                               //  webenginePage加载Channel功能
    ui->webEngine->setPage(page);                               //  ui显示该page。

在C++类Widget的构造函数,要进行准备,这里涉及了QWebEnginePage,QWebChannel,千万别乱,按照代码注释里清楚关系。这里有个QString类,定义了person,在返回去看js中的person,是不是明白了其中的联系了?!你可以写成任意可理解的字符。

紧接着就是槽函数了:必须是public slots: 不能是private slots

public slots:

    void getCoordinates(QString lon, QString lat) {
      
            QString tempLon="Mouse Lontitude:"+lon+"°";
            QString tempLat="Mouse Lattitude:"+lat+"°";
            ui->label_mouse_altitude->setText(tempLat);
            ui->label_mouse_latitude->setText(tempLon);

    };

经过以上,js会回传数据给lon和lat,然后你该怎么办就怎么办了。

2 总结

经过一下午的努力,完成了数据回传,可以将js的数据传递回来,但是QWebChannel这块还有其他的东西,比如和槽和信号连接,如果今后能遇到这个项目,我会继续研究,否则,到今天这个项目完毕。

感谢观看。

参考文献:

【木有】


版权声明:

1. 本文为MULTIBEANS团队研发跟随文章,未经允许不得转载。

2· 文中涉及的内容若有侵权行为,请与本人联系,本人会及时删除。

3· 尊重成果,本文将用的参考文献全部给出,向无私的工程师,爱好者致敬。


Linux进程之间的通信-管道(下)

Linux进程之间的通信-管道(下)

  • dup和管道函数
  • 命名管道:FIFO
  • 客户/服务器架构

1 dup和管道函数

1.1 dup

dup函数,复制文件句柄映射,fd2 = dup(fd1):

  • fd1和fd2是不一样的值;
  • write和read使用fd1和fd2是等效的;
  • close(fd1)不会影响fd2的使用。

image-20220326140351191

dup2函数,fd3 = dup2(fd1, fd2):

  • fd3和fd2是一样的值;
  • 如果fd2是一个已经打开的session,会被关闭,再复制fd1的链接;
  • fd2的值需要自己设定,并不是系统分配的,注意不要和已经open的fd重复了。

Note, std的文件描述符总是在使用最小可用的数字,例如,关闭掉std的文件描述符,那么文件描述符就会找到除了0以外最小的描述符。如下表格,如果我们close(0)之后,stdin的文件描述符被关闭,此时管道文件描述符使用stdin。

文件描述符 初始值 关闭文件描述符0后 dup调用之后
0 stdin [已关闭] 管道文件描述符
1 stdout stdout stdout
2 stderr stderr stderr
3 管道文件描述符 管道文件描述符 管道文件描述符
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
#define _GNU_SOURCE             /* See feature_test_macros(7) */
#include <fcntl.h>              /* Definition of O_* constants */
#include <unistd.h>
int dup3(int oldfd, int newfd, int flags);

Example:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

#define debug_log printf("%s:%s:%d--",__FILE__, __FUNCTION__, __LINE__);printf

int main(int argc, char **argv)
{
    int ret = 0;
    const char *buf1 = "hello my name is Carlos\n";
    const char *buf2 = "this is the string 2\n";
    const char *buf3 = "this is the string 3\n";
    const char *buf4 = "this is the string 4\n";
    const char *buf5 = "tfjfksdjflkdsjlkfjlksdjflkj\n";

    int origin_fd = 4, fd2 = 5, fd3 = 6, fd4 = 7, fd5 = 8, origin_fd_1 = 9;

    origin_fd = open("out_file.txt", O_RDWR|O_CREAT,0644);
    if (origin_fd == -1) {
        debug_log("origin_fd open failed, ret = %d\n", origin_fd);
        goto exit;
    }
    origin_fd_1 = open("out_file_2.txt", O_RDWR|O_CREAT,0644);
    if (origin_fd_1 == -1) {
        debug_log("origin_fd open failed, ret = %d\n", origin_fd);
        goto exit;
    }
    ret = write(origin_fd_1, buf1, strlen(buf1));
    debug_log("origin_fd write %d bytes on the file, origin_fd = %d\n", ret, origin_fd);

    // test dup, it is doesn't matter that close the original fd.
    fd2 = dup(origin_fd_1);                   // dup doesn't close the original fd.
    ret = write(fd2, buf2, strlen(buf2));
    debug_log("fd2 write %d bytes on the file, fd2 = %d\n", ret, fd2);
    ret = write(origin_fd, buf2, strlen(buf2));
    debug_log("origin_fd write %d bytes on the file\n", ret);

    // test dup2
    fd4 = dup2(fd2, fd3);                   // dup2, fd3 is fd4
    debug_log("fd2 = %d fd3 = %d, fd4 = %d\n", fd2, fd3, fd4);
    ret = write(fd3, buf3, strlen(buf3));
    debug_log("fd3 write %d bytes on the file\n", ret);
    ret = write(fd4, buf4, strlen(buf4));
    debug_log("fd4 write %d bytes on the file, fd3 = %d, fd4 = %d\n", ret, fd3, fd4);
    ret = write(fd2, "using fd2 rewrite file", 22);
    debug_log("fd2 write %d bytes on the file, fd2 = %d\n", ret, fd2);

    // re-directing the stdin
    fd5 = dup2(fileno(stdout), fd4);
    ret = write(fd5, buf5, strlen(buf5));
    debug_log("fd5 write %d bytes on the file, fd5 = %d\n", ret, fd5);


    close(fd2);
    close(fd3);
    close(fd4);
    close(fd5);
    debug_log("test finish!");

exit:
    return 0;
}

1.2 管道和dup

这里面可以利用fd的最小可用属性可以实现fork进程之间的stdout -> stdin管道通信。这里需要注意的是:

  • 进程fork之后,fd数量也会被复制,必须两个进程都要关闭才算真正意义的关闭。

image-20220326150524008

int test_pipe_rw_fork_dup()
{
    int ret = 0;
    int data_process = 0;
    int file_pipes[2];
    const char some_data[] = "hello world";
    char buffer[BUFSIZ + 1];
    pid_t pid = 0;
    memset(buffer, '\0', sizeof buffer);

    ret = pipe(file_pipes);
    if (ret != 0) {
        debug_log("pipe failed, ret = %d\n", ret);
        goto exit;
    }

    pid = fork();
    if (pid == -1) {
        debug_log("fork failed, ret = %d\n", ret);
        goto exit;
    }
    // child process
    else if (pid == 0) {
        // close STDIN
        close(0);
        // dup pipes[0] -> STDIN_FILENO
        dup(file_pipes[0]);
        close(file_pipes[0]);
        close(file_pipes[1]);
        execlp("od", "od", "-c", (char*) 0);
        debug_log("child wrote the bytes\n");
    }
    // parent process
    else {
        close(file_pipes[0]);
        data_process = write(file_pipes[1], some_data, strlen(some_data));
        close(file_pipes[1]);
        debug_log("parent write %d bytes: %s\n", data_process, some_data);
    }

exit:
    return ret;
}

输出:

# carlos @ Carloss-MBP in ~/workspace/work/clab/macos/test_pipe on git:master x [14:37:49] 
$ ./test.elf
/Users/carlos/workspace/work/clab/macos/test_pipe/test_pipe.c:test_pipe_rw_fork_dup:161--parent write 11 bytes: hello world
0000000    h   e   l   l   o       w   o   r   l   d                                                                            
0000013

2 命名管道:FIFO

之前的管道我们可以叫做匿名管道,匿名管道有个比较重要的特点:两个进程之间必须有共同的祖先进程,也就是必须要fork或者exec才能完成管道之间的通信。为了打破这个限制,Linux提供了命名FIFO管道,允许两个毫无关系的进程相互通信,而且这种通信的传输速率还是很高的。我们在Linux命令行中可以轻易的使用命名管道FIFO。

2.1 shell中的命名管道

  • 创建管道

    $ mkfifo ./fifo

image-20220327161734848

  • 读入管道的内容

    $ cat < ./fifo

    此时cat命令被阻塞,因为没有任何的数据被写入管道。

  • 写入管道内容

    $ echo "Hallow world! > ./fifo"

    此时在cat的命令终端就可以看到写入的数据了。

2.2 FIFO PIEP APIs

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);

这里面的mode还是有个点说法的:

  • O_RDONLY: 这种情况下open一个管道将会被阻塞,除非有一个进程以写的方式打开同一个FIFO。
  • O_RDONLY | O_NONBLOCK:没有任何进程以写的方式打开FIFO,open调用也立即成功并且返回。
  • O_WRONLY:这种情况下open一个管道会被阻塞,除非有一个进程以读的方式打开同一个FIFO。
  • O_WRONLY | O_NONBLOCK: 立刻返回,如果没有进程以读的方式打开FIFO,open调用将返回一个错误-1,并且FIFO不会被打开。

注意,在调用write和read函数对FIFO进行写读操作的时候,要注意对写进行“原子化”,每个写的长度保证小于等于PIPE_BUF(在limits.h文件中)字节,系统可以保证数据不会交错在一起,所以单次长度限制长度小于PIPE_BUF。

2.3 Example

实现两个进程之间使用FIFO管道交互:

  • 一个进程:创建FIFO,并且向FIFO管道中不断的写入数据
  • 一个进程,读FIFO管道的数据。

Server : Write data

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <limits.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>

#define debug_log printf("%s:%s:%d--",__FILE__, __FUNCTION__, __LINE__);printf


#define FIFO_NAME "./fifo"
#define BUFFER_SIZE PIPE_BUF
#define TEN_MEG (1024*1024*10)

int main(void)
{
    char buffer[BUFFER_SIZE + 1];
    int chars_read;
    int ret = 0;
    int fd = ~0;
    int bytes_sent = 0;
    int count = 0;

    if (access(FIFO_NAME, F_OK) == -1) {
        ret = mkfifo(FIFO_NAME, 0777);
        if (ret != 0) {
            debug_log("mkfifo failed ret = %d\n", ret);
            goto exit;
        }
    }
    debug_log("process %d opening fifo with O_WR_ONLY\n", getpid());
    fd = open(FIFO_NAME, O_WRONLY);
    if (fd < 0) {
        debug_log("open fifo file failed, ret = %d\n", fd);
        ret = -1;
        goto exit;
    }
    debug_log("process %d will send data by fifo\n", getpid());
    while(bytes_sent < TEN_MEG) {
        ret = write(fd, buffer, BUFFER_SIZE);
        if (ret == -1) {
            debug_log("write failed, ret = %d\n", ret);
            goto exit;
        }
        bytes_sent += ret;
        count ++;
        printf("sent %d count, sent bytes %d, sum %d\n", count, ret, bytes_sent);
    }
    close(fd);
    debug_log("process %d finished, %d bytes read\n", getpid(), bytes_sent);
exit:
    return 0;
}

Client: Read data

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <limits.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>

#define debug_log printf("%s:%s:%d--",__FILE__, __FUNCTION__, __LINE__);printf


#define FIFO_NAME "./fifo"
#define BUFFER_SIZE PIPE_BUF

int main(void)
{
    char buffer[BUFFER_SIZE + 1];
    int chars_read;
    int ret = 0;
    int fd = ~0;
    int bytes_read = 0;
    int count = 0;

    if (access(FIFO_NAME, F_OK) == -1) {
        debug_log("mkfifo failed ret = %d\n", ret);
        goto exit;
    }
    debug_log("process %d opening fifo with O_WR_ONLY\n", getpid());
    fd = open(FIFO_NAME, O_RDONLY);
    if (fd < 0) {
        debug_log("open fifo file failed, ret = %d\n", fd);
        ret = -1;
        goto exit;
    }
    debug_log("process %d will read data by fifo\n", getpid());
    do {
        ret = read(fd, buffer, BUFFER_SIZE);
        bytes_read += ret;
        count ++;
        printf("process read times %d, read bytes %d, sum %d\n", count, ret, bytes_read);
    } while(ret > 0);

    debug_log("process %d finished, %d bytes read\n", getpid(), bytes_read);
    close(fd);
exit:
    return 0;
}

image-20220327164039721

0x24_LinuxKernel_进程(一)进程的管理(生命周期、进程表示)

0x24_LinuxKernel_进程(一)进程的管理(生命周期、进程表示)

在UNIX操作系统下运行的应用程序、服务器以及其他程序都被称为进程。每个进程都在CPU的虚拟内存上分配地址空间。每个进程的地址空间都是独立的,因此进程和进程不会意识对方的存在,觉得自己是CPU上唯一运行的进程。从处理器的角度,系统上有几个CPU,就最多能运行几个进程。但Linux是一个多任务的系统,内核为了支持多任务需要在不同的进程之间切换,这样从时间维度的划分造成了多进程同时运行的假象。

内核借助CPU的帮助,负责进程切换,欺骗进程独占了CPU。这就需要在切换进程之前保存进程的所有状态及相关要素,并将进程置于IDLE状态。在进程切换回来之前,这些资源和信息需要恢复到CPU和内存上面。这些资源被成为进程上下文

内核除了要保存进程一些必要信息之外。还需要合理分配每一个进程的时间片,通常重要的进程得到CPU的时间多一点,次要的进程时间少一点。这个时间分配的过程成为调度

本文对于进程的表述的结构如下所示:

1. 用户空间定义

这部分介绍一些从用户空间视角来观摩进程的概念。

1.1 进程树

Linux对进程的表述采用了一种层次结构,每个进程都依赖于一个父进程。内核启动init程序作为第一个进程,该进程负责进一步的系统初始化操作,并显示视提示符和登陆界面。因此init进程被称为进程树的所有的进程都直接或者间接起源自该进程。如下使用pstree程序的输出所示。

这个树形结构的进程上面和新进程的创建方式密切相关。UNIX操作系统有两种创建新进程的机制分别是fork()exec()

fork()可以创建当前进程的一个副本且有独立的PID号码(父进程和子进程只有PID号码不同)。Linux使用了一个中所周的的技术来使fork更高效,那就是写时复制(copy on write)操作。

exec()将一个新程序加载到当前的内存中执行,旧程序的内存页面被逐出,其内容被替换为新数据,然后开始执行新的程序。

1.2 线程

除了重量级进程(有时候也成为UNIX进程),还有一个轻量级进程(线程)。本质上,一个进程包含若干个线程,这些线程使用着同样的地址空间,共享同样的数据和资源,因此除了使用同步的方法保护共享的资源之外,没有额外的通信机制了。这也是线程和进程的差别。

Linux使用clone()的方法创建线程。其工作方式类似于fork,但启用了精准的检查,以确认那些资源与父进程共享、哪些资源为线程独立创建。

1.3 用户空间进程编程

linux提供一些关于进程的系统调用,例如fork()getpid()getppid()wait()waitpid(),可以创建进程,对父进程和子进程流程的一些控制。

1.3.1 fork进程

https://github.com/carloscn/clab/blob/master/linux/test_pid/test_pid.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>

#define debug_log printf("%s:%s:%d--",__FILE__, __FUNCTION__, __LINE__);printf

int main(int argc, char *argv[])
{
    pid_t pid = fork();

    while(1) {
        if (0 == pid) {
            debug_log("Current PID = %d, PPID = %d\n", getpid(), getppid());
            debug_log("I'm Child process!\n");
            sleep(1);
        } else if (pid > 0) {
            debug_log("-------------------> Current PID = %d, PPID = %d\n", getpid(), getppid());
            debug_log("-------------------> I'm Parent process!\n");
            sleep(2);
        } else {
            debug_log("fork() failed \n");
        }
    }

    return 0;
}

两个进程会竞争一个终端输出:

但是后台是被fork了两个进程:

1.3.2 zombie进程

fork进程之后,子进程的处理程序中直接退出,而父进程处理程序中没有任何等待子进程的操作。这时候子进程就会成为僵尸进程。僵尸进程会被init 0收养,等待父进程退出之后,僵尸进程会被init 0进程释放掉。
https://github.com/carloscn/clab/blob/master/linux/test_pid/test_pid_zombie.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>

#define debug_log printf("%s:%s:%d--",__FILE__, __FUNCTION__, __LINE__);printf

int main(int argc, char *argv[])
{
    pid_t pid = fork();
    u_int16_t count = 0;

    while(1) {
        if (0 == pid) {
            debug_log("Current PID = %d, PPID = %d\n", getpid(), getppid());
            debug_log("I'm Child process\n");
            // just exit child process, and parent process don't call waitpid(),
            // which to create zombie process
            sleep(1);
            count ++;
            if (count > 10) {
                debug_log("child will exit.\n");
                exit(0);
            }
        } else if (pid > 0) {
            // debug_log("-------------------> wait for child exiting.\n");
            // WNOHANG is non block mode.
            // WUNTRACED is block mode.
            // mask the next line code, the zombie process will be generated.
            // pid_t t_pid = waitpid(pid, NULL, WUNTRACED);
            debug_log("-------------------> Current PID = %d, PPID = %d\n", getpid(), getppid());
            debug_log("-------------------> I'm Parent process!\n");
            sleep(2);
        } else {
            debug_log("fork() failed \n");
        }
    }
    return 0;
}

子进程退出之后,它成为僵尸进程。

父进程使用waitpid可以观测到子进程是否已经退出,以及时回收进程资源。

2. 进程的管理和调度

2.1 进程优先级

进程优先级粗暴分为实时进程和非实时进程:

  • **硬实时进程:**有严格的时间限制/Linux不支持硬实时/一些linux旁支版本RTLinux支持硬实时,主要原因是调度器没有做进内核中,内核作为一个独立的进程处理一些不仅要的任务。
  • Linux的任务优先满足吞吐量,所以弱化了进程调度。但是这些年人们也在降低内核延迟上面做了很多研究,比如提出可抢占机制、实时互斥内核锁还有完全公平调度器。
  • 软实时进程:类似与写CD这种工作种类的进程,就算是写进程被暂时中断也不会造成宕机之类的风险操作。
  • 普通进程: 没有具体的时间约束限制,但是会分配优先级来区分重要性。

进程优先级模型转换为时间片长度,优先级越高的占用的时间片越多。这种方案被称为抢占式多任务处理(preemptive multitasking),即各个进程都被分配到一定的时间片用来执行。时间到了之后,内核会从当前进程收回控制权,让不同的进程运行。抢占的时候所有的CPU寄存器的内容和页表都会保存起来,因此其结果并不会因为进程的切换而丢失。

2.2 进程的生命周期

2.2.1 状态机

一个进程是有状态机可以表示的,我们可以分为:**运行(running)、等待(waiting)、休眠(sleeping)和终止(stopped) **四个状态,如图所示:

状态机的切换是调度器来完成,切换条件可以解释为:

① Running -> Sleeping:如果进程必须等待事件,则状态变为“运行”直接切换到“睡眠”,但这个过程是不可逆的。
② Running -> Waiting: 调度器决定从进程收回资源的时候,过程状态从R变为W。
③ Sleeping -> Waiting:正如①所说,Sleeping没有办法直接变回Running,必须有一个中间状态waiting。
④ Waiting -> Running: CPU此时把资源分配给其他进程。在调度器授予CPU时间之前,进程一直保持waiting的状态。在分配CPU之后,其状态变为Running。

不在于周期范围内的进程:“僵尸进程(zombie)”:子进程被KILL信号(SIGTERM和SIGKILL)杀死,父进程没有调用wait4()。正常流程是,子进程被KILL,父进程调用wait4系统调用,通知内核子进程已死。处于僵尸进程状态进程:

  • 还会在进程表中(ps/top能刷出进程)
  • 占用很少的资源
  • 重启后才能刷掉该进程

2.2.2 抢占式多任务处理

  • 进程执行分为内核态和用户态,最大区别在于,内存地址区域访问划分不同。
  • 进内核态方法一:如果用户态进程进入内核态:访问共享数据,文件系统空间,必须通过系统调用,才能进入到内核态。
  • 进内核态方法二:中断触发进入内核态。
  • 中断触发和系统调用可以使用户态进入到内核态,但用户态是主动调用的,中断是外部触发的。
    • 用户态 < 核心态 < 中断
  • 进程抢占层次:
    • 普通进程总会被抢占
    • 进程处于内核态(或者普通进程处于系统调用期间)无法被任何进程抢占,但中断可以抢占内核态,因此要求中断不能占用太长时间
    • 中断可以暂停用户态进程,甚至和内核进程都可以被暂定
    • 内核抢占(kernel preemption)被加入到内核配置中,允许进程在紧急状态下抢占用户进程或者内核进程。优点:减少等待时间,让进程更“平滑”的执行;缺点:增加内核复杂程度。

2.3 进程的表示

Linux有一套自己对于进程管理的方式,还有调度器,调度器可以理解为真正去执行和指挥进程如何运行的东西,而管理方式可以说是调度器去管理进程的一个笔记计划表(task_struct)在include/sched.h中,这里有如何管理进程,进程命名,编号法等等。

2.3.1 task结构体

-Linux 通常把process當做是taskPCB (processing control block) 通常也稱為 struct tast_struct 1

https://elixir.bootlin.com/linux/v3.19.8/source/include/linux/sched.h#L1274

struct task_struct {
	volatile long state;	/* -1 unrunnable, 0 runnable, >0 stopped */
	void *stack;
	atomic_t usage;
	unsigned int flags;	/* per process flags, defined below */
	unsigned int ptrace;

#ifdef CONFIG_SMP
	struct llist_node wake_entry;
	int on_cpu;
	struct task_struct *last_wakee;
	unsigned long wakee_flips;
	unsigned long wakee_flip_decay_ts;

	int wake_cpu;
#endif
	int on_rq;

	int prio, static_prio, normal_prio;
	unsigned int rt_priority;
	const struct sched_class *sched_class;
	struct sched_entity se;
	struct sched_rt_entity rt;
#ifdef CONFIG_CGROUP_SCHED
	struct task_group *sched_task_group;
#endif
	struct sched_dl_entity dl;

#ifdef CONFIG_PREEMPT_NOTIFIERS
	/* list of struct preempt_notifier: */
	struct hlist_head preempt_notifiers;
#endif

#ifdef CONFIG_BLK_DEV_IO_TRACE
	unsigned int btrace_seq;
#endif

	unsigned int policy;
	int nr_cpus_allowed;
	cpumask_t cpus_allowed;

#ifdef CONFIG_PREEMPT_RCU
	int rcu_read_lock_nesting;
	union rcu_special rcu_read_unlock_special;
	struct list_head rcu_node_entry;
#endif /* #ifdef CONFIG_PREEMPT_RCU */
#ifdef CONFIG_PREEMPT_RCU
	struct rcu_node *rcu_blocked_node;
#endif /* #ifdef CONFIG_PREEMPT_RCU */
#ifdef CONFIG_TASKS_RCU
	unsigned long rcu_tasks_nvcsw;
	bool rcu_tasks_holdout;
	struct list_head rcu_tasks_holdout_list;
	int rcu_tasks_idle_cpu;
#endif /* #ifdef CONFIG_TASKS_RCU */

#if defined(CONFIG_SCHEDSTATS) || defined(CONFIG_TASK_DELAY_ACCT)
	struct sched_info sched_info;
#endif

	struct list_head tasks;
#ifdef CONFIG_SMP
	struct plist_node pushable_tasks;
	struct rb_node pushable_dl_tasks;
#endif

	struct mm_struct *mm, *active_mm;
#ifdef CONFIG_COMPAT_BRK
	unsigned brk_randomized:1;
#endif
	/* per-thread vma caching */
	u32 vmacache_seqnum;
	struct vm_area_struct *vmacache[VMACACHE_SIZE];
#if defined(SPLIT_RSS_COUNTING)
	struct task_rss_stat	rss_stat;
#endif
/* task state */
	int exit_state;
	int exit_code, exit_signal;
	int pdeath_signal;  /*  The signal sent when the parent dies  */
	unsigned int jobctl;	/* JOBCTL_*, siglock protected */

	/* Used for emulating ABI behavior of previous Linux versions */
	unsigned int personality;

	unsigned in_execve:1;	/* Tell the LSMs that the process is doing an
				 * execve */
	unsigned in_iowait:1;

	/* Revert to default priority/policy when forking */
	unsigned sched_reset_on_fork:1;
	unsigned sched_contributes_to_load:1;

#ifdef CONFIG_MEMCG_KMEM
	unsigned memcg_kmem_skip_account:1;
#endif

	unsigned long atomic_flags; /* Flags needing atomic access. */

	pid_t pid;
	pid_t tgid;

#ifdef CONFIG_CC_STACKPROTECTOR
	/* Canary value for the -fstack-protector gcc feature */
	unsigned long stack_canary;
#endif
	/*
	 * pointers to (original) parent process, youngest child, younger sibling,
	 * older sibling, respectively.  (p->father can be replaced with
	 * p->real_parent->pid)
	 */
	struct task_struct __rcu *real_parent; /* real parent process */
	struct task_struct __rcu *parent; /* recipient of SIGCHLD, wait4() reports */
	/*
	 * children/sibling forms the list of my natural children
	 */
	struct list_head children;	/* list of my children */
	struct list_head sibling;	/* linkage in my parent's children list */
	struct task_struct *group_leader;	/* threadgroup leader */

	/*
	 * ptraced is the list of tasks this task is using ptrace on.
	 * This includes both natural children and PTRACE_ATTACH targets.
	 * p->ptrace_entry is p's link on the p->parent->ptraced list.
	 */
	struct list_head ptraced;
	struct list_head ptrace_entry;

	/* PID/PID hash table linkage. */
	struct pid_link pids[PIDTYPE_MAX];
	struct list_head thread_group;
	struct list_head thread_node;

	struct completion *vfork_done;		/* for vfork() */
	int __user *set_child_tid;		/* CLONE_CHILD_SETTID */
	int __user *clear_child_tid;		/* CLONE_CHILD_CLEARTID */

	cputime_t utime, stime, utimescaled, stimescaled;
	cputime_t gtime;
#ifndef CONFIG_VIRT_CPU_ACCOUNTING_NATIVE
	struct cputime prev_cputime;
#endif
#ifdef CONFIG_VIRT_CPU_ACCOUNTING_GEN
	seqlock_t vtime_seqlock;
	unsigned long long vtime_snap;
	enum {
		VTIME_SLEEPING = 0,
		VTIME_USER,
		VTIME_SYS,
	} vtime_snap_whence;
#endif
	unsigned long nvcsw, nivcsw; /* context switch counts */
	u64 start_time;		/* monotonic time in nsec */
	u64 real_start_time;	/* boot based time in nsec */
/* mm fault and swap info: this can arguably be seen as either mm-specific or thread-specific */
	unsigned long min_flt, maj_flt;

	struct task_cputime cputime_expires;
	struct list_head cpu_timers[3];

/* process credentials */
	const struct cred __rcu *real_cred; /* objective and real subjective task
					 * credentials (COW) */
	const struct cred __rcu *cred;	/* effective (overridable) subjective task
					 * credentials (COW) */
	char comm[TASK_COMM_LEN]; /* executable name excluding path
				     - access with [gs]et_task_comm (which lock
				       it with task_lock())
				     - initialized normally by setup_new_exec */
/* file system info */
	int link_count, total_link_count;
#ifdef CONFIG_SYSVIPC
/* ipc stuff */
	struct sysv_sem sysvsem;
	struct sysv_shm sysvshm;
#endif
#ifdef CONFIG_DETECT_HUNG_TASK
/* hung task detection */
	unsigned long last_switch_count;
#endif
/* CPU-specific state of this task */
	struct thread_struct thread;
/* filesystem information */
	struct fs_struct *fs;
/* open file information */
	struct files_struct *files;
/* namespaces */
	struct nsproxy *nsproxy;
/* signal handlers */
	struct signal_struct *signal;
	struct sighand_struct *sighand;

	sigset_t blocked, real_blocked;
	sigset_t saved_sigmask;	/* restored if set_restore_sigmask() was used */
	struct sigpending pending;

	unsigned long sas_ss_sp;
	size_t sas_ss_size;
	int (*notifier)(void *priv);
	void *notifier_data;
	sigset_t *notifier_mask;
	struct callback_head *task_works;

	struct audit_context *audit_context;
#ifdef CONFIG_AUDITSYSCALL
	kuid_t loginuid;
	unsigned int sessionid;
#endif
	struct seccomp seccomp;

/* Thread group tracking */
   	u32 parent_exec_id;
   	u32 self_exec_id;
/* Protection of (de-)allocation: mm, files, fs, tty, keyrings, mems_allowed,
 * mempolicy */
	spinlock_t alloc_lock;

	/* Protection of the PI data structures: */
	raw_spinlock_t pi_lock;

#ifdef CONFIG_RT_MUTEXES
	/* PI waiters blocked on a rt_mutex held by this task */
	struct rb_root pi_waiters;
	struct rb_node *pi_waiters_leftmost;
	/* Deadlock detection and priority inheritance handling */
	struct rt_mutex_waiter *pi_blocked_on;
#endif

#ifdef CONFIG_DEBUG_MUTEXES
	/* mutex deadlock detection */
	struct mutex_waiter *blocked_on;
#endif
#ifdef CONFIG_TRACE_IRQFLAGS
	unsigned int irq_events;
	unsigned long hardirq_enable_ip;
	unsigned long hardirq_disable_ip;
	unsigned int hardirq_enable_event;
	unsigned int hardirq_disable_event;
	int hardirqs_enabled;
	int hardirq_context;
	unsigned long softirq_disable_ip;
	unsigned long softirq_enable_ip;
	unsigned int softirq_disable_event;
	unsigned int softirq_enable_event;
	int softirqs_enabled;
	int softirq_context;
#endif
#ifdef CONFIG_LOCKDEP
# define MAX_LOCK_DEPTH 48UL
	u64 curr_chain_key;
	int lockdep_depth;
	unsigned int lockdep_recursion;
	struct held_lock held_locks[MAX_LOCK_DEPTH];
	gfp_t lockdep_reclaim_gfp;
#endif

/* journalling filesystem info */
	void *journal_info;

/* stacked block device info */
	struct bio_list *bio_list;

#ifdef CONFIG_BLOCK
/* stack plugging */
	struct blk_plug *plug;
#endif

/* VM state */
	struct reclaim_state *reclaim_state;

	struct backing_dev_info *backing_dev_info;

	struct io_context *io_context;

	unsigned long ptrace_message;
	siginfo_t *last_siginfo; /* For ptrace use.  */
	struct task_io_accounting ioac;
#if defined(CONFIG_TASK_XACCT)
	u64 acct_rss_mem1;	/* accumulated rss usage */
	u64 acct_vm_mem1;	/* accumulated virtual memory usage */
	cputime_t acct_timexpd;	/* stime + utime since last update */
#endif
#ifdef CONFIG_CPUSETS
	nodemask_t mems_allowed;	/* Protected by alloc_lock */
	seqcount_t mems_allowed_seq;	/* Seqence no to catch updates */
	int cpuset_mem_spread_rotor;
	int cpuset_slab_spread_rotor;
#endif
#ifdef CONFIG_CGROUPS
	/* Control Group info protected by css_set_lock */
	struct css_set __rcu *cgroups;
	/* cg_list protected by css_set_lock and tsk->alloc_lock */
	struct list_head cg_list;
#endif
#ifdef CONFIG_FUTEX
	struct robust_list_head __user *robust_list;
#ifdef CONFIG_COMPAT
	struct compat_robust_list_head __user *compat_robust_list;
#endif
	struct list_head pi_state_list;
	struct futex_pi_state *pi_state_cache;
#endif
#ifdef CONFIG_PERF_EVENTS
	struct perf_event_context *perf_event_ctxp[perf_nr_task_contexts];
	struct mutex perf_event_mutex;
	struct list_head perf_event_list;
#endif
#ifdef CONFIG_DEBUG_PREEMPT
	unsigned long preempt_disable_ip;
#endif
#ifdef CONFIG_NUMA
	struct mempolicy *mempolicy;	/* Protected by alloc_lock */
	short il_next;
	short pref_node_fork;
#endif
#ifdef CONFIG_NUMA_BALANCING
	int numa_scan_seq;
	unsigned int numa_scan_period;
	unsigned int numa_scan_period_max;
	int numa_preferred_nid;
	unsigned long numa_migrate_retry;
	u64 node_stamp;			/* migration stamp  */
	u64 last_task_numa_placement;
	u64 last_sum_exec_runtime;
	struct callback_head numa_work;

	struct list_head numa_entry;
	struct numa_group *numa_group;

	/*
	 * numa_faults is an array split into four regions:
	 * faults_memory, faults_cpu, faults_memory_buffer, faults_cpu_buffer
	 * in this precise order.
	 *
	 * faults_memory: Exponential decaying average of faults on a per-node
	 * basis. Scheduling placement decisions are made based on these
	 * counts. The values remain static for the duration of a PTE scan.
	 * faults_cpu: Track the nodes the process was running on when a NUMA
	 * hinting fault was incurred.
	 * faults_memory_buffer and faults_cpu_buffer: Record faults per node
	 * during the current scan window. When the scan completes, the counts
	 * in faults_memory and faults_cpu decay and these values are copied.
	 */
	unsigned long *numa_faults;
	unsigned long total_numa_faults;

	/*
	 * numa_faults_locality tracks if faults recorded during the last
	 * scan window were remote/local. The task scan period is adapted
	 * based on the locality of the faults with different weights
	 * depending on whether they were shared or private faults
	 */
	unsigned long numa_faults_locality[2];

	unsigned long numa_pages_migrated;
#endif /* CONFIG_NUMA_BALANCING */

	struct rcu_head rcu;

	/*
	 * cache last used pipe for splice
	 */
	struct pipe_inode_info *splice_pipe;

	struct page_frag task_frag;

#ifdef	CONFIG_TASK_DELAY_ACCT
	struct task_delay_info *delays;
#endif
#ifdef CONFIG_FAULT_INJECTION
	int make_it_fail;
#endif
	/*
	 * when (nr_dirtied >= nr_dirtied_pause), it's time to call
	 * balance_dirty_pages() for some dirty throttling pause
	 */
	int nr_dirtied;
	int nr_dirtied_pause;
	unsigned long dirty_paused_when; /* start of a write-and-pause period */

#ifdef CONFIG_LATENCYTOP
	int latency_record_count;
	struct latency_record latency_record[LT_SAVECOUNT];
#endif
	/*
	 * time slack values; these are used to round up poll() and
	 * select() etc timeout values. These are in nanoseconds.
	 */
	unsigned long timer_slack_ns;
	unsigned long default_timer_slack_ns;

#ifdef CONFIG_FUNCTION_GRAPH_TRACER
	/* Index of current stored address in ret_stack */
	int curr_ret_stack;
	/* Stack of return addresses for return function tracing */
	struct ftrace_ret_stack	*ret_stack;
	/* time stamp for last schedule */
	unsigned long long ftrace_timestamp;
	/*
	 * Number of functions that haven't been traced
	 * because of depth overrun.
	 */
	atomic_t trace_overrun;
	/* Pause for the tracing */
	atomic_t tracing_graph_pause;
#endif
#ifdef CONFIG_TRACING
	/* state flags for use by tracers */
	unsigned long trace;
	/* bitmask and counter of trace recursion */
	unsigned long trace_recursion;
#endif /* CONFIG_TRACING */
#ifdef CONFIG_MEMCG
	struct memcg_oom_info {
		struct mem_cgroup *memcg;
		gfp_t gfp_mask;
		int order;
		unsigned int may_oom:1;
	} memcg_oom;
#endif
#ifdef CONFIG_UPROBES
	struct uprobe_task *utask;
#endif
#if defined(CONFIG_BCACHE) || defined(CONFIG_BCACHE_MODULE)
	unsigned int	sequential_io;
	unsigned int	sequential_io_avg;
#endif
#ifdef CONFIG_DEBUG_ATOMIC_SLEEP
	unsigned long	task_state_change;
#endif
};

成员非常多,弄清楚费劲,可以分类:

  • 状态和执行信息,如待决信号、进程的二进制格式种类、进程PID、父进程地址、优先级和CPU时间。
  • 分配的虚拟内存信息。
  • 身份凭据,如用户ID、组ID和权限等。
  • 使用的文件(包括程序代码的二进制文件)
  • 线程信息记录,CPU的运行时间数据。
  • 与其他进程通信有关的信息。
  • 该进程所用的信号处理,用于响应到来的信号。

2.3.2 进程状态机

<sched.h>
stcuct task_struct {
	volatile long state;
};
  • TASK_RUNNING: 处于可运行状态,未必处于CPU cover时间也有可能是等待调度器的状态。
  • TASK_INTERRUPTIBLE: 针对某时间或者其他资源的睡眠进程设置的。
  • TASK_UNINTERRUPTIBLE:用于内核指示而停用的睡眠进程。不能由外部唤醒,只能由内核亲自唤醒。
  • TASK_STOPPED:进程特意停止运行,例如,由调度器暂停。
  • TASK_TRACED:本来不是进程状态,用于停止的进程(ptrace机制)与常规停止进程区分开。
  • EXIT_ZOMBIE:僵尸状态
  • EXIT_DEAD:wait系统调用已发出,解除僵尸状态。

2.3.3 进程资源限制

<sched.h>
<resource.h>
struct rlimit {
    unsigned long rlim_cur;
    unsigned long rlim_max;
}
struct task_struct {
	struct rlimite limit;
};
  • rlim_cur: 进程当前的资源限制,成为软限制(soft limit)
  • rlim_max:该限制的最大容许值, 硬限制(hard limit)

用户进程通过setrlimit()系统调用来增减当前限制,最大值不能超过rlim_max;getrlimits()用于检查当前限制。那么设定的资源是什么呢?

cat /proc/self/limits来查看当前系统设定进程的资源限制。

Linux系统启动的时候会设定好当前资源限制的属性,在include/asm-generic/resource.h中定义进程的资源限制,在linux启动的时候通过init进程完成配置。

在init_task.h中挂载该数组。

2.3.4 进程类型

进程是由(二进制代码应用程序)、(单线程)、分配给应用程序的资源(内存、文件)。新进程是使用(fork)或者(exec)系统调用产生的。

  • fork生成当前进程的副本,称为子进程,复制为两份一样的独立的进程,资源是分开的,资源都是copy的两份,不再系统上做任何关联。
  • exec从一个可执行二进制加载另一个应用程序,来替代当前运行的进程。exec不是创建新的进程,首先使用fork复制一份旧的程序,然后调用exec在系统上创建一个应用程序。
  • 旧版本的clone调用,原理和fork一致,可以共享父进程的一些资源,但属于线程范畴

2.3.5 命名空间

命名空间是解决进程之间权限访问问题的一种设计机制。Linux的全局管理特性,比如PID、UID和系统调用uname返回系统的信息都是全局调用。因此就导致资源和重用的问题,在虚拟化中亟需解决。把全局资源通过命名空间抽象出来,划分不同的命名空间对应不同的资源分配,可实现虚拟化环境。

父命名空间生成多个子命名空间,子命名空间有映射关系到父命名空间。

a) 命名空间的数据结构

#include <nsproxy.h>
struct nsproxy {
    atomic_t count;
    struct uts_namespace *uts_ns; //uts: 内核名称、版本、底层体系结构
	struct ipc_namespace *ipc_ns; //ipc: 进程间通信有关的信息
	struct mnt_namespace *mnt_ns; //已装在的文件系统的视图 stcuct mnt_namespace
	struct pid_namespace *pid_ns; //有关进程ID的信息 struct pid_namespace
	struct user_namespace *user_ns; // 保存用于限制每个用户资源使用的信息
	struct net *net_ns;// 包含所有网络相关的命名空间参数
};

创建新进程的使用使用fork可以建立一个新的命名空间,所以在fork的时候需要标记创建新的命名空间的类别。

#include <sched.h> 
#define CLONE_NEWUTS 0x04000000 /* 创建新的utsname组 */ 
#define CLONE_NEWIPC 0x08000000 /* 创建新的IPC命名空间 */ 
#define CLONE_NEWUSER 0x10000000 /* 创建新的用户命名空间 */ 
#define CLONE_NEWPID 0x20000000 /* 创建新的PID命名空间 */ 
#define CLONE_NEWNET 0x40000000 /* 创建新的网络命名空间 */

在task_struct里面有有关于命名空间的定义:

struct task_struct {
    //...
	struct nsproxy *nsproxy;
    //...
};

struct task_struct里面挂的是stcut nsproxy的指针,只要挂上不同命名空间的指针,就算是赋予不同的命名空间了。

NOTE: 命名空间的支持必须在编译的时候启动 General setup -> Namespaces support,而且必须逐一指定需要支持的命名空间。如果内核编译的时候没有指定命名空间的支持,默认的命名空间的作用则类似于不启用命名空间,所有的属性相当于全局的。

b) UTS命名空间和用户命名空间

<kernel/nsproxy.c> 
struct nsproxy init_nsproxy = INIT_NSPROXY(init_nsproxy); 

<init_task.h> 
#define INIT_NSPROXY(nsproxy) { \ 
    .pid_ns = &init_pid_ns, \ 
    .count = ATOMIC_INIT(1), \ 
    .uts_ns = &init_uts_ns, \ 
    .mnt_ns = NULL, \ 
    INIT_NET_NS(net_ns) \ 
    INIT_IPC_NS(ipc_ns) \ 
    .user_ns = &init_user_ns, \ 
}
b.1) UTS命名空间

UTS命名空间是Linux内核Namespace(命名空间)的一个子系统,主要用来完成对容器HOSTNAME和domain的隔离,同时保存内核名称、版本、以及底层体系结构类型等信息。

<utsname.h> 
struct uts_namespace { 
    struct kref kref; 
    struct new_utsname name; 
};
// 在proc中可以看到这些值
struct new_utsname { 
    char sysname[65];    //  cat /proc/sys/kernel/ostype
    char nodename[65];   
    char release[65];    //  cat /proc/sys/kernel/osrelease
    char version[65];    //  cat /proc/sys/kernel/version
    char machine[65]; 
    char domainname[65]; // cat /proc/sys/kernel/domainname
};

init/version.c 
struct uts_namespace init_uts_ns = { 
... 
    .name = { 
        .sysname = UTS_SYSNAME, 
        .nodename = UTS_NODENAME, 
        .release = UTS_RELEASE, 
        .version = UTS_VERSION,
        .machine = UTS_MACHINE, 
        .domainname = UTS_DOMAINNAME, 
	}, 
};

就一个名字和kref,引用计数器,可以跟踪内核中有多少地方使用了struct uts_namespace的实例。

b.2) 用户命名空间

用户命名空间在数据结构管理方面类似于UTS:在要求创建新的用户命名空间时,则生成当前用户命名空间的一份副本,并关联到当前进程的nsproxy实例。但用户命名空间自身的表示要稍微复杂一些:

<user_namespace.h> 
    struct user_namespace { 
    struct kref kref; 
    struct hlist_head uidhash_table[UIDHASH_SZ]; 
    struct user_struct *root_user; 
};

<kernel/user_namespace.c> 
static struct user_namespace *clone_user_ns(struct user_namespace *old_ns) 
{ 
    struct user_namespace *ns; 
    struct user_struct *new_user; 
    ... 
    ns = kmalloc(sizeof(struct user_namespace), GFP_KERNEL); 
    ... 
    ns->root_user = alloc_uid(ns, 0); 
    /* 将current->user替换为新的 */ 
    new_user = alloc_uid(ns, current->uid); 
    switch_uid(new_user); 
    return ns; 
}

2.3.6 进程ID号

我们耳熟能详的PID(process id)在其命名空间中唯一的标识号码,我们也可以通过PID找到一个进程,对进程进行操作。在进程的领域,并不是只有PID,也有其他很多的ID类型。这些概念延伸至进程组2

Linux对于PID是要进行管理的,而管理手段对PID进行数据结构的表述,这些数据结构一只脚踏入task_struct中,还有一只脚在命名空间映射,而且对于繁琐的PID的数据结构查找,Linux内核也提供了若干辅助函数用于扫描PID的数据结构。

PID的分配,首要保证的是PID在命名空间上的唯一性,内核提供了这样的方法,alloc_pidmapfree_pidmap这样的方法可以分配和释放PID。

a) 进程组的TGID

(1)进程组

也称之为作业,BSD与1980年前后向UNIX中增加的一个新特性,代表一个或多个进程的集合。每个进程都属于一个进程组,在waitpid函数和kill函数的参数中都曾经使用到,操作系统设计的进程组的概念,是为了简化对多个进程的管理。

当父进程创建子进程的时候,默认子进程与父进程属于同一个进程组,进程组ID等于进程组第一个进程ID(组长进程)。所以,组长进程标识:其进程组ID等于其进程ID.

组长进程可以创建一个进程组,创建该进程组的进程,然后终止,只要进程组中有一个进程存在,进程组就存在,与组长进程是否终止无关。

(2)kill发送给进程组

使用 kill -n -pgid 可以将信号 n 发送到进程组 pgid 中的所有进程。例如命令 kill -9 -4115 表示杀死进程组 4115 中的所有进程。

每个进程除了PID之外还有TGID(进程组的ID),组长(group leader)的PID = TGID。

<sched.h>
struct task_struct {
    ...
    pid_t pid;
    pid_t tgid;
    ...
}

在userspace提供setpgidgetpgrpgetpgid的系统调用。

pid_t getpgrp(void); /*获取当前进程的进程组ID*/
pid_t getpgid(pid_t pid); /*改变进程默认所属的进程组,通常可用来加入一个现有的进程组或新进程组。*/
int setpgid(pid_t pid, pid_t pgid); /* 改变进程默认所属的进程组,通常可用来加入一个现有的进程组或新进程组。*/

这部分可以做一个实验,https://github.com/carloscn/clab/blob/master/linux/test_pid/test_process_grp.c

b) 管理PID

除了pidtgid还需要其他成员来管理PID,如下结构体:

<pid.h>
struct pid_namespace {
    ...
    struct task_struct *child_reaper;
    ...
    int level;
    struct pid_namespace *parent;
};

struct upid {
    int nr;
    struct pid_namespace *ns;
    struct hlist_node pid_chain;
};

struct pid {
    atomic_t count;
    /* lists of tasks that use this pid */
    struct hlist_head tasks[PIDTYPE_MAX];
    int level;
    struct upid numbers[1];
};

<pid.h>
enum pid_type
{
    PIDTYPE_PID,
    PIDTYPE_PGID,
    PIDTYPE_SID,
    PIDTYPE_MAX
};

c) 管理PID函数

//kernel/pid.c
pid_t task_pid_nr_ns(struct task_struct *tsk, struct pid_namespace *ns)
pid_t task_tgid_nr_ns(struct task_struct *tsk, struct pid_namespace *ns)
pid_t task_pgrp_nr_ns(struct task_struct *tsk, struct pid_namespace *ns)
pid_t task_session_nr_ns(struct task_struct *tsk, struct pid_namespace *ns)

生成唯一的PID:

//kernel/pid.c
static int alloc_pidmap(struct pid_namespace *pid_ns)
static fastcall void free_pidmap(struct pid_namespace *pid_ns, int pid)

d) 进程关系

除了ID链接之外,内核还负责管理建立在UNIX进程创建模型之上的“家族关系”。术语如下:

  • 如果是进程A分支形成了B,那么A成为B的父进程,而B成为A的子进程。
  • 如果A分支形成了B1,B2,B3,...,Bn,这些Bx称为兄弟关系。

// <sched.h>
struct task_struct {
	...
	struct list_head children; /* list of my children */
	struct list_head sibling; /* linkage in my parent’s children list */
	...
}
  • children是链表表头,该链表保存所有进程的子进程;
  • sibling用于将兄弟进程彼此联系起来。

task_struct之间的互相联系可以表现如图所示3

3. 进程管理相关的系统调用

在这部分,我们来讨论forkexec的系统调用实现。这部分其实在# 13_ARMv8_内存管理(一)-内存管理要素 有提及,但是是从内存管理的角度来看(内存分页技术在fork上面提供了写时复制功能,为其效率提升做了很大的贡献)。在这一节我们来了解一下fork如何实现的。

同时,我们把fork的系统调用实现,归纳到# 0x21_LinuxKernel_内核活动(一)之系统调用 的“可用系统调用”章节中去。

3.1 进程复制

在Linux系统中,不仅仅只有一个fork系统调用,还有其他系统调用,例如vforkclonefork进程依赖于写时复制技术的实现。forkvforkclone调用的内核函数分别是sys_forksys_vforksys_clone函数。三个函数最终也都调用了do_fork函数,这个函数内部大多数工作都是由copy_process复制进程的内核函数完成的。

进程内部至少包含一个线程,因此进程复制中比较重要的处理过程就是对于线程的复制。每个线程都有自己独立的栈空间,因此这部分要很谨慎小心的处理。

进程复制涉及方方面面,比如对于进程一些共享信息的处理(内存),shmat这样的系统调用就强调了在进程复制之前,复制后的行为4

3.1.1 写时复制

关于进程的系统调用分为:

  • fork():属于重量级调用,因为其建立了一个完整的进程副本,然后作为子进程去执行。为了减少工作量,linux内核使用了写时复制的的技术。由于写时复制技术的实现,vfork速度方面不再有优势,应避免使用vfork。
  • vfork():类似于fork(),但是不会创建一个进程副本,父进程和子进程之间共享一份数据。好处就是节约了大量的CPU,而坏处是,父进程或者子进程之间任意一个成员修改数据都能影响到对方。vfork的设计用于子进程形成之后立即执行execve系统调用5加载新程序的情形。在子进程退出或者加载新程序之前,内核保证父进程处于阻塞状态
  • clone():产生线程,可以对父子进程之间的共享、复制进行精准控制。

写时复制(Copy-on-write, COW)技术,防止了父进程被复制之后建立真的数据,避免了使用大量的内存,复制时操作耗费更多的时间。如果复制之后执行exec立即加载程序,那么负面效应更加严重,甚至复制都是多余的,因为exec会把新数据的内存替换到当前进程的内存位置,然后运行。

写时复制技术在底层实现的是在fork的时候,只复制父进程的页表,而不是复制真正的数据,而且父子进程不允许修改彼此的物理页(共享内存的物理页除外), 这种实现很简单,通过标记页表为只读属性,无论父子进程在尝试访问内存的时候,由于访问了只读属性的页面,处理器会向内核报告访问段错误,接着在段错误的处理函数中开始真正的复制数据。

COW机制使得内核可能尽可能的延迟内存的复制,更重要的是,实际上在很多情况下不需要复制,这样节约了大量的时间。

3.1.2 执行系统调用

do_fork

do_forkhttps://elixir.bootlin.com/linux/v3.19.8/source/kernel/fork.c#L1626

//kernel/fork.c
long do_fork(unsigned long clone_flags, /* 控制复制过程 */
            unsigned long stack_start, /* 用户状态下的起始地址 */
            struct pt_regs *regs,      /* 指向寄存器集合的指针 */
            unsigned long stack_size,  /* 用户栈大小,不必要,设为0 */
            int __user *parent_tidptr, /* 用户空间中地址的两个指针\ */
            int __user *child_tidptr)  /* 分别指向父子进程的PID */

NPTL(Native Poxsix Threads Library)库线程需要实现这个两个参数。6

参数 描述
clone_flags 与clone()参数flags相同, 用来控制进程复制过的一些属性信息, 描述你需要从父进程继承那些资源。该标志位的4个字节分为两部分。最低的一个字节为子进程结束时发送给父进程的信号代码,通常为SIGCHLD;剩余的三个字节则是各种clone标志的组合(本文所涉及的标志含义详见下表),也就是若干个标志之间的或运算。通过clone标志可以有选择的对父进程的资源进行复制;
stack_start 与clone()参数stack_start相同, 子进程用户态堆栈的地址
regs 是一个指向了寄存器集合的指针, 其中以原始形式, 保存了调用的参数, 该参数使用的数据类型是特定体系结构的struct pt_regs,其中按照系统调用执行时寄存器在内核栈上的存储顺序, 保存了所有的寄存器, 即指向内核态堆栈通用寄存器值的指针,通用寄存器的值是在从用户态切换到内核态时被保存到内核态堆栈中的(指向pt_regs结构体的指针。当系统发生系统调用,即用户进程从用户态切换到内核态时,该结构体保存通用寄存器中的值,并被存放于内核态的堆栈中)
stack_size 用户状态下栈的大小, 该参数通常是不必要的, 总被设置为0
parent_tidptr 与clone的ptid参数相同, 父进程在用户态下pid的地址,该参数在CLONE_PARENT_SETTID标志被设定时有意义
child_tidptr 与clone的ctid参数相同, 子进程在用户太下pid的地址,该参数在CLONE_CHILD_SETTID标志被设定时有意义

linux-4.2之后选择引入一个新的CONFIG_HAVE_COPY_THREAD_TLS,和一个新的COPY_THREAD_TLS接受TLS参数为额外的长整型(系统调用参数大小)的争论。改变sys_clone的TLS参数unsigned long,并传递到copy_thread_tls。新版本的系统中clone的TLS设置标识会通过TLS参数传递, 因此_do_fork替代了老版本的do_fork。

#ifndef CONFIG_HAVE_COPY_THREAD_TLS
/* For compatibility with architectures that call do_fork directly rather than
 * using the syscall entry points below. */
long do_fork(unsigned long clone_flags,
              unsigned long stack_start,
              unsigned long stack_size,
              int __user *parent_tidptr,
              int __user *child_tidptr)
{
        return _do_fork(clone_flags, stack_start, stack_size,
                        parent_tidptr, child_tidptr, 0);
}
#endif

sys_fork

从设计层次来看,sys_fork是架构级定义(在arch/xxx/kernel目录下),调用linux/kernel下实现的do_fork实现。

早期内核2.4.31版本都在自己的架构目录上实现:

架构 实现
arm arch/arm/kernel/sys_arm.c, line 239
i386 arch/i386/kernel/process.c, line 710
x86_64 arch/x86_64/kernel/process.c, line 703
asmlinkage long sys_fork(struct pt_regs regs)
{
    return do_fork(SIGCHLD, regs.rsp, &regs, 0);
}

新版本例如4.1.15版本的内核把sys_fork已经去掉,换成:

#ifdef __ARCH_WANT_SYS_FORK
SYSCALL_DEFINE0(fork)
{
#ifdef CONFIG_MMU
        return _do_fork(SIGCHLD, 0, 0, NULL, NULL, 0);
#else
        /* can not support in nommu mode */
        return -EINVAL;
#endif
}
#endif

我们可以看到唯一使用的标志是SIGCHLD。这意味着在子进程终止后将发送信号SIGCHLD信号通知父进程。由于写时复制(COW)技术,最初父子进程的栈地址相同,但是如果操作栈地址闭并写入数据,则COW机制会为每个进程分别创建一个新的栈副本。如果do_fork成功,则新建进程的pid作为系统调用的结果返回,否则返回错误码。

sys_vfork

早期内核2.4.31版本都在自己的架构目录上实现:

架构 实现
arm arch/arm/kernel/sys_arm.c, line 254
i386 arch/i386/kernel/process.c, line 737
x86_64 arch/x86_64/kernel/process.c, line 725
asmlinkage long sys_vfork(struct pt_regs regs)
{
    return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs.rsp, &regs, 0);
}

同样,新版本例如4.1.15版本的内核把sys_vfork已经去掉,换成:

#ifdef __ARCH_WANT_SYS_VFORK
SYSCALL_DEFINE0(vfork)
{
        return _do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0,
                        0, NULL, NULL, 0);
}
#endif

可以看到sys_vfork的实现与sys_fork只是略微不同, 前者使用了额外的标志CLONE_VFORK | CLONE_VM

sys_clone

早期内核2.4.31版本都在自己的架构目录上实现:

架构 实现
arm arch/arm/kernel/sys_arm.c, line 247
i386 arch/i386/kernel/process.c, line 715
x86_64 arch/x86_64/kernel/process.c, line 708
asmlinkage int sys_clone(struct pt_regs regs)
{
    /* 注释中是i385下增加的代码, 其他体系结构无此定义
    unsigned long clone_flags;
    unsigned long newsp;

    clone_flags = regs.ebx;
    newsp = regs.ecx;*/
    if (!newsp)
        newsp = regs.esp;
    return do_fork(clone_flags, newsp, &regs, 0);
}

同样,新版本例如4.1.15版本的内核把sys_clone已经去掉,换成:

#ifdef __ARCH_WANT_SYS_CLONE
#ifdef CONFIG_CLONE_BACKWARDS
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
                 int __user *, parent_tidptr,
                 unsigned long, tls,
                 int __user *, child_tidptr)
#elif defined(CONFIG_CLONE_BACKWARDS2)
SYSCALL_DEFINE5(clone, unsigned long, newsp, unsigned long, clone_flags,
                 int __user *, parent_tidptr,
                 int __user *, child_tidptr,
                 unsigned long, tls)
#elif defined(CONFIG_CLONE_BACKWARDS3)
SYSCALL_DEFINE6(clone, unsigned long, clone_flags, unsigned long, newsp,
                int, stack_size,
                int __user *, parent_tidptr,
                int __user *, child_tidptr,
                unsigned long, tls)
#else
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
                 int __user *, parent_tidptr,
                 int __user *, child_tidptr,
                 unsigned long, tls)
#endif
{
        return _do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr, tls);
}
#endif

我们可以看到sys_clone的标识不再是硬编码的,而是通过各个寄存器参数传递到系统调用,因而我们需要提取这些参数。其次,clone也不再复制进程的栈,而是可以指定新的栈地址,在生成线程时,可能需要这样做,线程可能与父进程共享地址空间, 但是线程自身的栈可能在另外一个地址空间。 另外,还指令了用户空间的两个指针(parent_tidptr和child_tidptr), 用于与线程库通信6

3.1.3 进程的生命周期

下图是一个进程的生命周期和相应的探针点:

Unix 进程生成分为两个阶段3

  • 父进程调用 fork() 系统调用。kernel 创建一个父进程的副本, 包括地址空间(在 copy-on-write 模式下),打开的文件,分配一个新的 PID。 如果 fork() 调用成功,这个将返回在两个进程的上下文中,这个有同一个指令指针(PC 指针是一样的) 在子进程中随后的代码通常用来关闭文件,重置信号等。
  • 子进程调用 execve() 系统调用,这个将使用新的 based 传递给 execve() 来替换掉进程的地址空间。

当调用 exit() 系统调用,子进程将结束。 但是,进程也可以会被 killed,当 kernel 出现不正确的条件(引发 kernel oops) 或者机器错误。 如果父进程像等待子进程结束,这个可以调用 wait() 系统调用(或者 waitid()), wait() 调用将收到进程的退出码,随后关联的 task_struct 将被销毁。 如果父进程不像等待子进程,子进程退出后,这个将被作为僵尸进程。 父进程可能会收到 kernel 发送的 SIGCHLD 信号。

3.1.4 do_fork的实现

do_fork overview

do_fork无论是最新版还是老的linux版本,都是内核级的实现,这部分给架构级的sys_fork函数调用。从这里可以看出,这部分已经不是和平台相关的代码了,纯属内核内部的软件逻辑。kernel/fork.c

  • do_fork以调用copy_process开始,后者执行新进程的实际工作(收尾工作)。我们暂时不去理会copy_process内部做了什么。
  • 确定PID,这部分涉及两种逻辑。有无创建新的命名空间,fork如果创建了新的命名空间,则调用pid_nr_ns;如果没有创建命名空间只在局部PID获取即可task_pid_vnr
  • 如果进程使用ptrace监控新的进程,创建进程之后还要发送SIGSTOP信号,以便调试器检查其数据。
  • 关于调度方面,子进程使用wake_up_new_task唤醒。
  • 在fork进程的时候为了防止父进程修改数据,父进程需要阻塞,通过完成机制丰富设计。

copy_process

copy_processdo_fork中核心函数,任务是完成父进程的复制功能,这里面必须包含三个系统调用的请求处理fork\vfork\clone

这个复制过程比较复杂,我们大部分过程略过,具体解析参考《深入Linux内核架构》P55-P61,原版参考P66-P75。

thread问题

父进程的PCB实例只有一个成员不同:新进程分配了一个新的内核态栈,task_struct->stack。通常栈和thread_info保存在一个联合体中,thread_info保存了线程所需要的所有特定处理器的底层信息。

<sched.h>
union thread_union {
	struct thread_info thread_info;
	unsigned long stack[THREAD_SIZE/sizeof(long)];
};

// <asm-arch/thread_info.h>
struct thread_info {
	struct task_struct *task; /* main task structure */
	struct exec_domain *exec_domain; /* execution domain */
	unsigned long flags; /* low level flags */
	unsigned long status; /* thread-synchronous flags */
	__u32 cpu; /* current CPU */
	int preempt_count; /* 0 => preemptable, <0 => BUG */
	mm_segment_t addr_limit; /* thread address space */
	struct restart_block restart_block;
}

关系可以看:

在内核的某个特定组件使用了过多的栈空间的时候,内核栈就会溢出到thread_info上,这可能会出现严重的故障。在这种情况下,调用栈回溯的时候就会导致错误的信息出现,因此内核提供了kstack_end函数,用于判断给出的地址是否位于栈的有效部分。

Linux 並沒有特定的data structure來標示thread or process,thread與process都使用process的PCB。

3.2 内核线程

在linux系统中, 我们接触最多的莫过于用户空间的任务,像用户线程或用户进程,因为他们太活跃了,也太耀眼了以至于我们感受不到内核线程的存在,但是内核线程却在背后默默地付出着,如内存回收,脏页回写,处理大量的软中断等,如果没有内核线程那么linux世界是那么的可怕!7在进入我们真正的主题之前,我们需要知道一下事实:

  • 内核线程永远运行于内核态绝不会跑到用户态去执行。
  • 由于内核线程运行于内核态,所有它的权限很高,请注意这里说的是权限很高并不意味着它的优先级高,所有他可以直接做到操作页表,维护cache, 读写系统寄存器等操作。
  • 内核线性是没有地址空间的概念,准确的来说是没有用户地址空间的概念,使用的是所有进程共享的内核地址空间,但是调度的时候会借用前一个进程的地址空间。
  • 内核线程并没有什么特别神秘的地方,他和普通的用户任务一样参与系统调度,也可以被迁移到任何cpu上运行。
  • 每个cpu都有自己的idle进程,实质上也是内核线程,但是他们比较特殊,一来是被静态创建,二来他们的优先级最低,cpu上没有其他进程运行的时候idle进程才运行。
  • 除了初始化阶段0号内核线程和kthreadd本身,其他所有的内核线程都是被kthreadd内核线程来间接创建。

我们知道linux所有任务的祖先是0号进程,然后0号进程创建了天字第一号的1号init进程,init进程是所有用户任务的祖先,而内核线程同样也有自己的祖先那就是kthreadd内核线程他的pid是2,我们通过top命令可以观察到:红色方框都是父进程为2号进程的内核线程,绿色方框为kthreadd,他的父进程为0号进程。

这里面有一个知识点惰性TLB(lazy TLB),把task_struct里面的mm_struct设定为空指针将成为惰性TLB进程。假设内核线程之后运行的进程与之前是同一个,在这种情况下,内核并不需要修改用户空间的地址表,地址表转换后备缓冲器(TLB)中的信息仍然有效。只有在内核线程执行的进程是与此前不同的用户层才需要切换,清除TLB数据。

创建内核线程的辅助方法是,kthread_create

kernel/kthread.c
struct task_struct *kthread_create(int (*threadfn)(void *data),
				   void *data, const char namefmt[], ...)
}

该函数创建一个新的内核线程,命名为namefmt。最初线程是挺值得,需要使用wake_up_process启动它。词汇会调用kthreadfn给出线程函数。

另一个备选方案是通过kthread_run宏来代替。

使用ps fax可以输出方括号的进程为内核线程所属进程,与普通进程区分。


3.3 启动新程序

Linux提供execve系统调用启动新的程序,通过新代码替换现存程序。

3.3.1 execve实现

该系统调用的入口节点是sys_execve函数,早期内核2.4.31版本都在自己的架构目录上实现:

架构 实现
arm arch/arm/kernel/sys_arm.c, line 262
i386 arch/i386/kernel/process.c, line 475
x86_64 arch/x86_64/kernel/process.c, line 678
asmlinkage int sys_execve(char *filenamei, char **argv, char **envp, struct pt_regs *regs)
{
	int error;
	char * filename;

	filename = getname(filenamei);
	error = PTR_ERR(filename);
	if (IS_ERR(filename))
		goto out;
	error = do_execve(filename, argv, envp, regs);
	putname(filename);
out:
	return error;
}
  • 首先要打开可执行文件,内核找到相关的inode生成一个文件描述符;
  • bprm_init处理几个管理型的任务,包括mm_struct初始化,mm_init用于栈创建;
  • prepare_binprm:用于提供一些父进程相关的值UID和GID;
  • 剩下是处理参数的列表,环境文件名等。Linux支持执行文件的各种不同组织,标准格式是ELF。
  • search_binary_handler用于do_execve结束之后查找一个适当的二进制格式,用于执行特定的文件。
    • 释放原始进程的所有资源。
    • 将应用程序映射到虚拟地址空间中。必须考虑下列段的处理:
      • text段包含程序的可执行代码。start_codeend_code指定该段在地址空间驻留区域。
      • 预先初始化数据(在编译阶段指定了具体值的变量)位于start_dataend_data之间。
      • 堆空间用于动态内存分配。start_brkbrk指定边界。
      • 栈位置start_stack定义。几乎所有的寄存机栈都是自动向下增长的。唯一的例外是PA-RISC。对于栈反向增长,体系结构相关部分的实现必须告知内核,可以通过设定STACK_GROWSUP完成。
      • 程序的环境变量和参数也需要映射到虚拟空间。arg_startarg_end之间,还有env_endenv_start

除了ELF格式,还有几种Linux支持的二进制格式,这里列举作为参考:

3.3.2 解释二进制

在linux内核中,每种二进制都表示下列数据结构的实例:

<binfmts.h>
struct linux_binfmt {
    struct linux_binfmt * next;
    struct module *module;
    int (*load_binary)(struct linux_binprm *, struct pt_regs * regs);
    int (*load_shlib)(struct file *);
    int (*core_dump)(long signr, struct pt_regs * regs, struct file * file);
    unsigned long min_coredump; /* minimal dump size */
};
  • load_binary:用于加载普通程序
  • load_shlib:用于加载共享库
  • core_dump:用于在程序错的情况下输出内存转储。内存转储随后可以使用调试器,例如gdb分析,以便解决问题。
  • min_coredump是生成内存转储时,内存转储文件长度的下界。

注意每种二进制格式首先必须使用resgister_binfmt像内核注册。该函数的目的是向一个链表增加一种新的二进制格式。

3.3.3 退出进程

进程必须exit系统调用终止。这使得内核有机会将该进程的资源释放回系统。该调用入口点是sys_exit函数,需要一个错误码作为其参数。很快退出进程调度委托给do_exit。简言之,该函数的实现就是将各个引用计数器-1。

4. 参考文献

Footnotes

  1. # 奔跑吧 CH 3.1 進程的誕生

  2. 【进程】进程组__月雲之霄的博客-CSDN博客__进程组

  3. Process management 2

  4. Linux进程之间的通信-内存共享(System V)

  5. execve

  6. Linux下进程的创建过程分析(_do_fork/do_fork详解)--Linux进程的管理与调度(八) 2

  7. 深入理解Linux内核之内核线程(上)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.