carloscn / blog Goto Github PK
View Code? Open in Web Editor NEWMy blog
License: Apache License 2.0
My blog
License: Apache License 2.0
有几个涉及的专有名词概念
这里面有几个一般性的规则:
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文件
研究编译文件,从一个最简单的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)
使用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
还有个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
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
我们要找到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]
....
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
把文件代码段化,使用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
__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 分别为我们自己映射的区域。
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
加法指令有ADD、ADDS、ADCS。 ADD一般性加法指令,ADCS带C标志位运算的加法指令,ADDS影响C标志位的加法运算。
a = a + b, 没有进位标志,也不会利用进位标志
ADD <Xd|SP>, <Xn|SP>, <Wm>, {<extend> {#<amount>}}
add x0, x1, x2
( x0 = x1 + x2 )add x0, x1, x2, lsl #5
( x0 = x1 + (x2 << 5) )ADD <Xd|SP>, <Xn|SP>, #<imm>{, lsl <#shift>}
, note shift supports #0 and #12 only.add x1, x2, #8
(x1 = x2 + 8)add x1, x2, #8, lsl #12
( x1 = x2 + (8 << 12) )ADD <Xd>, <Xn>, <Xm>{, <shift> #<amount>}
, note #amount range 0 to 63add x1, x2, x3, asr #2
(a,C) = a + b, 带进位标志的加法,用法和ADD一样
(a,C) = a + b + C,带进位标志的加法,且需要加上C标志位,用法和ADD一样。 注意,如果加法溢出的时候C标志位会置位为1,比如,a = 0xFFFFFFFFFFFFFFFF, b = 1,此时,加法溢出,C置位1。
a = b + PC, 当前程序的PC值加上给定的地址偏移
ADR <Xd>, <label>
adr x1, #25
msr NZCV, xzr // clear the NZCV
mrs x0, NZCV // 查看NZCV寄存器,NZCV在高位28 - 32 bits
adcs x0, zxr, xzr
让两个0寄存器相加 0+0+c就可以得到C标志位的值
减法指令包含SBC,SBCS。请参考ARMv8手册,C6.2.231 C6-1299
a = a - b, 没有进位标志,也不会利用进位标志。使用方法和ADD一致。
(a,N) = a - b, 会置标志位N。使用方法和SUBS一致,减成负数的时候,其余位置补1。
a = a - b - 1 + C
SBC <Xd>, <Xn>, <Xm>
sbc x0, xzr, xzr
(a, N) = a - b - 1 + C, 如果减出负数的话,N会被置位
SBCS <Xd>, <Xn>, <Xm>
sbcs x0, xzr, x1
比较指令,实际上也使用SBC实现的, cmp x1, x2
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
位操作包含AND, ANDS, ORR、EOR、BFI、UBFX、SBFX, 分别是与、与置位标志位、或、异或、插入、无符号提取、有符号提取。
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
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的小技巧:
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
(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
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
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
[1] Arm Armv8-A Architecture Registers-NZCV, Condition Flags
[2] ARM Cortex-A Series Programmer's Guide for ARMv8-A - Arithmetic and logical operations
最近一段时间都在恶补数据结构和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最底层说起。
平台 | 内容 |
---|---|
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接口) |
按照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再使用。
目标板终端输入: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方法进行数据的交互。
随手搜了一下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内核的严谨、严肃的态度。也再一次说,不能懒惰,自己查手册,看最标准的说明。
我这里给出最简单的模型,其他的字符驱动注册什么的同spi驱动,这里只说明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多行,结果不断的尝试,发现就这些。
下面就说几个重点:
adapter = i2c_get_adapter(0);
定义一个指针,然后使用i2c_get_adapter(0),得到我们上面说的,i2c-0,这个adapter。你疑问了,我为什么选择i2c-0这个adapter,为什么不选择-i2c-其他。因为这个开发板只把-i2c-0的引脚印出来了。
。。。
这样就获取到了adapter。
接着我们就要创建一个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完成了,很简单,可是探索起来好麻烦。
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的指针。
读相比于写就费劲多了,但是也没难到哪里去,只不过是两条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的值了。
验证函数:
#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日
#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端程序对于用户来讲ad9833_dev.c ad9833_server.c main.c 三个主要的文件,
环境搭建正确之后,最核心的就是这三个东西,对还有个makefile要配置正确。我在环境调试的时间花的比开发时间多的多,最重要的就是要环境配置正确,库啊,路径啊,这类的。
我们这里给出接口函数目录,具体实现不给出:
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_server里面运行。
AD9833这块就不多说了,我们主要来说多核通信这块的知识。
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( ¶ms );
params.mode = Semaphore_Mode_COUNTING;
Semaphore_construct(&server->sem_obj,0,¶ms);
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;
}
}
}
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程序,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.
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.)
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 |
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
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
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
我们在03_ELF文件_静态链接里面提到了对于一般性Linux的最终的ELF文件的内部结构,包含了各种段,然后相同的符号是如何进行链接的,怎么生成一个ELF文件的。在Linux里面,使用aarch64-linux-gnu-gcc可以说是一个比较high-level的编译器,里面有个默认的链接配置,而如果使用aarch64-none-elf-gcc这种baremental环境下的编译器,可能就要自己进行链接和写一些链接脚本。我们研究ARMv8的gcc、ld的编译算是对ELF_静态链接的一个扩展,来更熟悉链接这个过程,加深对于ELF文件的理解。
链接器英文是Linker,用于把多个目标文件的代码段、数据段、符号表等链接到一起组合成为一个二进制文件的bin-utils工具。为什么叫LD?根据文献2,LD的原名叫做,loaDer和Link eDitor,所以简写为LD。我们来研究一下ld的命令行:
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 | 在输出文件中定义指定的全局符号 |
-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
观察发现,.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
看到两个.text段,一个是a.o的text,一个是b.o的text段。如果在linker脚本中不指定lc的位置,那么紧凑排列的。
上面所有命令都是-e main指定程序入口的地址,在链接脚本中也可以指定使用ENTRY()内置函数。
ENTRY(main)
SECTIONS{
. = 0x100000;
.text : { a.o(.text) }
. = 0x200000;
.text : { b.o(.text) }
. = 0x800000;
.data : { *(.data) }
. = 0x900000;
.bss : { *(.bss) }
}
优先级:
我们在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) }
}
甚至还可以在这里面 + - * /的计算地址。
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
这样我们需要把0x1060地址的内容加载到0x2000的位置。
extern char _etext, _data, _edata, _bstart, _bend;
char *src = &_etext;
char *dst = &_data;
while(dst < &_edata)
*dst ++ = *src++;
ABSOLUTE: 绝对值
. = 0xb00000;
my_offset1 = ABSOLUTE(0x100); // 等于0x100
my_offset2 = 0x100; // 等于0xb00100
ADDR:返回虚拟地址
ALIGN: 声明下一个align对齐的地址
SIZEOF: 一个段的大小
现在就有个问题了,比如我们编译计算机的程序,如果elf文件都把运行地址定好了,那么就会存在一个问题,不同的机型有着不同的内存结构,而且不同的机器上面运行的app也不一样,我们elf文件中锁定的地址说不定已经被某个应用占用了,那么这个问题如何解决的呢?肯定有一个机制去管理这些地址,让这些地址可以根据自己机器的运行状况,使用内存大小做了一个相对化的处理。我们需要了解三个地址:
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
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,这种情况下如果程序想要启动,需要把代码段从加载地址复制到链接地址。
我们实际的嵌入式设备有很多ROM和RAM的存储器,有这么多VMA、LMA、LKA3之类的也是因为有不同的加载stage导致的。我们理解,首先代码有编译阶段->存储到嵌入式NorFlash或者NandFlash阶段->装载DDR阶段->装载SRAM阶段->CPU执行阶段
B指令没办法实现在链接地址运行的程序,只能用LDR对PC寄存器修改值。后面MMU还会对这边进行比较深入的解析。
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
软件环境:Vivado 2017.02 Linux版本
系统环境:Ubuntu 16.04 amd64
交叉编译器: gcc-linaro-7.3-2018.05.tar.xz
我的交叉编译环境放在/opt/toolschain/linaro/bin/arm-linux-gnueabihf-下,我编译的时候喜欢指定绝对编译器路径
获取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文件。
大体流程就是:用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文件。
到目前位置,拿到了:
可以开始合成BOOT.bin文件了
这个操作还是在sdk软件里面进行。
使用create boot image功能:
到此完成BOOT.bin的合成。
还有一个非常重要的事情,我试了很多次,zynq平台就是不启动,uboot也不输出任何的信息。这个小小的问题卡了我很久,不过在今天早上洗漱的时候,突然想到,Xilinx Vivado和SDK都是在root情况下启动,生成BOOT.bin也是可能有权限问题。所以....
我拿到板子,然后在SD卡里面,给定sudo chmod 777 BOOT.bin
然后弹出SD卡,把SD放在ZYNQ上,居然成功启动了。如果你是在Linux系统下,不要忘记给定BOOT.bin权限。
git clone https://github.com/Xilinx/linux-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 xilinx_zynq_defconfig
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
make CROSS_COMPILE=/opt/toolschain/linaro/bin/arm-linux-gnueabihf- ARCH=arm uImage LOADADDR=0x00008000
在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 = <ðernet_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文件,同样留着备用。
随便找个位置新建一个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
保存,留着备用。
准备一张空白的超过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卡制作。
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日
dup函数,复制文件句柄映射,fd2 = dup(fd1):
dup2函数,fd3 = dup2(fd1, fd2):
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;
}
这里面可以利用fd的最小可用属性可以实现fork进程之间的stdout -> stdin管道通信。这里需要注意的是:
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
之前的管道我们可以叫做匿名管道,匿名管道有个比较重要的特点:两个进程之间必须有共同的祖先进程,也就是必须要fork或者exec才能完成管道之间的通信。为了打破这个限制,Linux提供了命名FIFO管道,允许两个毫无关系的进程相互通信,而且这种通信的传输速率还是很高的。我们在Linux命令行中可以轻易的使用命名管道FIFO。
创建管道
$ mkfifo ./fifo
读入管道的内容
$ cat < ./fifo
此时cat命令被阻塞,因为没有任何的数据被写入管道。
写入管道内容
$ echo "Hallow world! > ./fifo"
此时在cat的命令终端就可以看到写入的数据了。
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
这里面的mode还是有个点说法的:
注意,在调用write和read函数对FIFO进行写读操作的时候,要注意对写进行“原子化”,每个写的长度保证小于等于PIPE_BUF(在limits.h文件中)字节,系统可以保证数据不会交错在一起,所以单次长度限制长度小于PIPE_BUF。
实现两个进程之间使用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;
}
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/
在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
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模式)
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:
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. |
arm的指令集有以下几种:
Uses 32-bit instruction encodings and fixed-length.
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
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.
64-bit dedicated Stack Pointer register.
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.
消息队列是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. |
消息队列和内存共享都有互相传递消息的可能性,但这个话题主要是想讨论的是什么时候用内存共享合适,什么时候用消息队列合适。我们从文献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的通信
场景二:一对多的通信,可以指定优先级
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())
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:
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);
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三个命令。
创建两个程序,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;
}
OMAPL对应3个版本的linux源代码,分别是:Linux-3.3、Linux-2.6.37、Linux2.6.33,这里的差距在于Linux2,缺少SYSLINK支持组件。
这里我们选择Linux-3.3版本进行开发。
sudo apt-get install u-boot-tools
apt-get install libncurses5-dev
arm-none-linux-gnueabi
的环境解压内核到~/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 ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabi- menuconfig
直接Exit就可以,然后就会写入配置信息了。
在这个menu里面我们配置驱动或者内核其他的组件机制,是否编译到内核中,这个根据自己需求进行,初学者可能对于这个地方没有多少概念。可参考韦东山《嵌入式Linux完全开发手册》的编译内核章节,上面有目录规则和解说等。
make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabi- -j4
make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabi- uImage -j4
编译内核一般都会有各种各样的错误,去百度或者google清查。
编译完成后可以在内核源码的"arch/arm/boot"目录下找到 uImage。可以将 uImage 拷贝到正常使用的 SD 系统启动卡的 FAT32 格式的 boot 分区,方便以后 SD 卡启动方式时被 U-Boot 加载或者被烧写到 NAND FLASH 分区。
在嵌入式的道路上寻寻觅觅很久,进入嵌入式这个行业也有几年的时间了,从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
本开发驱动基于Linux3.3内核版本,且内核必须编译正确,否则不能运行。
这个Demo可以归结为三个部分,一个部分为Linux字符驱动模板,第二部分为AD9833驱动程序,第三部分为通信协议。还附加一个配置文件。
本Demo就围绕这三点进行。
主要负责进行数据交互的。当设备生成字符设备驱动节点(/dev目录下),使用shell级命令cat或者编译一段C应用程序用open打开节点的时候,后面将参数就是通过ioctl函数进行传递。(在嵌入式Linux端定义一个ioctl的函数,在C语言的程序也有一个ioctl用来和其进行对应,这样就完成了数据参数传递。)
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主要的作用就是向内核注册该驱动。
内核级的嵌入式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函数,主要进行对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的驱动了。
到此,基本上就是裸机嵌入式的知识了,对于芯片功能的描述,对于芯片时序的把握。作为本博客不在赘述,给出函数的列表,如果喜欢,本文将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;
}
该设备使用链表进行描述。
在参考文献[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[] )
驱动开发完毕,就必须要将驱动编入Linux内核代码树,假如Linux内核代码在./linux-3.3目录,我们的驱动名字叫做ad9833.c,那么我们就要将ad9833.c文件放入./linux-3.3/drivers/char目录下,操作两件事情。
修改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]
在文末追加
obj-$(CONFIG_AD9833_ADI) += ad9833.o
这里CONFIG_后面接的必须和上面的Kconfig中 config字段一样 ad9833.o 的.o文件必须和放入该内核代码的ad9833.c名字字段一样。
make CROSS_COMPILE=arm-none-linux-gnueabi- ARCH=arm menuconfig
make CROSS_COMPILE=arm-none-linux-gnueabi- ARCH=arm -j8
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内核
【导语】:昨天编译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都是重新配置,然而并不是这样。 **
下载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文件,在程序内标定注意事项:
arch
命令查看)#
# 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 权限)注意几个关键点:
#!/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的路径考培就好了。
就是把我们刚才编译的环境拷贝到我们的目标板的目录下,可以使用FTP,可以使用OTG挂在U盘的方式,我喜欢使用SSH协议的scp命令。
sudo scp -r /home/delvis/work/qt5.1.1 [email protected]:/opt
输入密码之后就可以把整个文件夹传输到开发板上面了。
我们传输完毕之后,则需要在开发板上设定环境,当我们运行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
完成配置。
我们利用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· 尊重成果,本文将用的参考文献全部给出,向无私的工程师,爱好者致敬。
栈和堆的概念非常重要,程序员的修养是以x86架构讲的堆栈的概念,我们以ARMv8 AArch64为主来研究一下堆栈。
栈的概念我们可以重力翻转之后的桌子上的一摞书为例子,栈顶就是最下面眼镜的位置,栈底就是桌子。栈的顺序就是我们最后放的眼镜,是先被拿出来的。栈(stack)是一种数据结构,计算机里面的栈使用栈数据结构管理内存。为什么要将“重力翻转”?因为栈是一种从高地址向低地址生长的存储结构,栈底对应高地址,栈顶对应低地址。
这里的SP被称为“堆栈帧(Stack Frame)”或者“活动记录(Activate Record)”。堆栈帧会保存以下记录:
入栈过程:
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
使用aarch64-none-elf-gcc编译器参数-fomit-frame-pointer
可以取消帧指针:
使用fomit-frame-pointer的反汇编可以看到,123行sp已经不会备份到x29。
函数调用方和被调用方需要按照统一的协议去压栈和出栈,否则会有问题。调用惯例
__attribute__((cdecl))
我们定义一个这样的函数,有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;
}
这段函数的反汇编是:
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
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
前8个参数被压入寄存器中,后面的参数被直接压到栈中。返回参数被放在x0中,返回地址在x30中。 参考:02_ARMv7-M_编程模型与模式
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;
}
反汇编:
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,栈指针和返回地址存入栈空间,然后函数返回后弹出栈。
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
函数调用标准(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 | 用于传递子函数参数和结果, |
堆的概念我们已经知道了,而且我们还用过大名鼎鼎的malloc函数,甚至malloc_align函数,但是我们似乎没有研究过在Linux里面malloc原理是什么样子的,在今天的这个topic我们再进一步的了解一下堆,后面我们在学习linux内核的内存管理的时候会更详细的讲解一下malloc如何实现的。
Linux进程地址空间,除了文件、共享库还有栈之外,剩余的未分配的空间都可以作为Heap的空间地址,堆和栈相反,堆是向上增长的。运行库向操作系统申请一批空间地址,又程序自己“零售”给内部程序。
Linux进程堆管理有两种方式:
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函数处理逻辑是这样的:
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使用,会促进内存碎片化加剧,最终内存不足。
嵌入式设备没有MMU,无法实现内存动态映射。所以没有操作系统兜底的嵌入式设备一定要小心,就算是有操作系统也要对内存分配了如指掌,否则就会出现意想不到的问题,内存碎片的问题就是很头疼的问题。
freeRTOS对于堆的管理分为5个heap管理方式6,十分复杂。
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);
}
[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.
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
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.
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
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.
代表它所在的地址,也可以当做变量或者符号。
.global xxxxx
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
声明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 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
:下面的代码push接到指定的section当中。
.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的表,地址和函数名的对应关系:
请在汇编里定义一个类似的表,在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;
}
.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
在汇编文件通过如下两个函数:
然后写一个宏定义
.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
Linux用户空间的多线程和同步是一个非常重要的概念,即便在工程中能够利用这部分内容,但是还需要更系统化的对这部分进行了解,因为我们花一些时间来整理这部分的笔记。Linux用户空间的多线程为POSIX线程,与进程之间的区别就不再赘述了。多线程的优点在于比进程有着更小的开销,且共享信息更为便利,而带来的缺点难于同步、线程交互控制和调试。
线程中引出一个可重入的概念,可重入 <reentrant, [ˌriˈɛntrənt]>,用于避免类似于fputs之类的函数,这些函数通常会有全局性的缓冲区存储数据。这也提示我们如果要编写一个reentrant的函数,对于修改全局性的缓冲存储数据,要格外的留心。另外,在编写reentrant的程序时,需要_REENTRANT
宏定义来告诉编译器我们需要可重入的功能。reentrant还有另外一些知识点,在Linux中为了reentrant安全做了一些操作:
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:
API Define:
#include <pthread.h>
void pthread_exit (void *retval);
Parameters:
Params | I/O | Details |
---|---|---|
void *retval | Output | 返回指向某个对象的指针,注意不可以返回一个指向局部变量的指针,局部变量在栈回溯后会消失。 |
Return:
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.
请参考:test1_basic_thread on the link
这里需要注意编译选项:
对于第一个编译选项,如果系统默认是NPTL线程库,无需-lpthread
;-D_REENTRANT
默认会使用线程安全的实现。
这里塑造一个测试场景,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 !!
线程执行的过程完全是随机的。
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_open和sem_unlink
例子:
!
分析:
mainloop处理fgets字符串之后存入work_area,如果不加线程同步机制,thread01和thread02不断的竞争,最终达到稳态的时候,thread01写入1个!,thread02就会把他清掉。最后备份区域要么是0,要么是1个!。因此需要加同步机制,需要顺序是:
因此,mainloop任务之后需要post一个信号量sem_01, 在t01的handler之后需要post一个信号量sem_02。t01 wait,mainloop的信号量;t02 wait t01的信号量。
请参考:test2_thread_sem.c on the link
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的死锁场景:
使用mutex的死锁场景:
在ARMv7架构(单核imx6ULL芯片测试)
似乎CPU并没有跑满,原因暂时未知。
我觉得这个文献里面对比pthread_mutex和pthread_spinlock的调度性能对比可以参考一下1
这里面实验为:
在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: 脱离线程,该线程完成任务直接退出,不需要汇报主线程。可以配置下列宏定义
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: 调度由属性明确地设置
scope:线程调度的计算方式
stacksize: 设置线程的栈大小,字节,定义_POSIX_THREAD_ATTR_STACKSIZE的时候才会支持该属性的设定。Linux目前支持的线程栈很大,所以这个功能对于linux来说有点多余。
接口如下4:
int pthread_cancel(pthread_t thread);
int pthread_setcancelstate(int state, int *oldstate);
int pthread_setcanceltype(int type, int *oldtype);
对于setcanceltype:
除了mutex和spinlock,在pthread层级接口还提供了条件变量,和mutex打配合完成更高效的线程与线程之间的同步。比如这样的场景,小明爸爸每隔三天会向桌子上放一个苹果,小明可以一天吃一个苹果,小明不知道爸爸什么时候来放苹果,所以小明的策略是写作业写一个小时就去桌子上查看一下是不是有苹果,这样效率就十分低。条件变量就可以提供一个通知机制,小明爸爸放苹果之后,通知小明去取,小明再去桌子拿苹果吃,这样显著提高效率。
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状态。
我相信,关于多线程会有很多TOPIC值的探讨,这里遇到什么TOPIC都会整理下来。
之前我以为多核调度是进程层面的事情,也很好奇,后来查了多方材料,原来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个核心,会根据实际配置核心输出。
#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单核)上面测试得到不一样的结论:
注意: 环境为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(第三方库文件夹)
以上这些文件,全部存在~/setup文件下
参考我的博客:(基于OMAPL:Linux3.3内核的编译)[https://www.cnblogs.com/sigma0/p/9149041.html]
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 全称是"Qt Widgets for Technical Applications",是一个基于 LGPL 版权协议的开源
项目,可生成各种统计图。QWT的编译需要基于上一章节编译QT,编译出的qmake编译器
准备qwt-6.1.0.tar.bz2文件,解压到~/work
路径下:
tar -xvf qwt-6.1.0.tar.bz2 -C ~/work
需要修改两个地方:
在 qwt 目录下执行以下命令产生 Makefile 编译文件:
/opt/qt-arm-4.8.3/bin/qmake
ls
如果有Makefile文件则表示配置成功。
在 qwt 根目录下执行以下命令编译 qwt 组件源码:
make -j4
启动4个线程编译
编译完成后:
在 qwt 根目录下执行以下命令安装 qwt 组件:
sudo make install
该组件会解压到QWT_INSTALL_PREFIX指定的路径中:/opt/qwt-6.1.0
将"/opt/qwt-6.1.0/lib"下的所有文件拷贝到开发板文件系统"/usr/lib"目录下,用SD卡也可以,用scp命令也可以。
安装过程请参考我发在贴吧上的教程:我这里用的是CCS5.5版本,大同小异 (在LINUX(ubuntu)系统下装CCSv6方法)[https://tieba.baidu.com/p/3698761357]
注意路径安装到 /opt/ti下
执行:sudo ./OMAPL138_StarterWare_1_10_04_01-Linux-x86-Install
安装路径为: /opt/ti下
MCSDK是多核通信组件。
准备好mcsdk_1_01_00_02_setuplinux.bin,注意路径安装到/opt/ti下,完全安装就好。
sudo ./mcsdk_1_01_00_02_setuplinux.bin
进入"mcsdk_1_01_00_02"目录下,启动 MCSDK 设置脚本,根据不同主机设置,进行tftp、nfs、U-Boot 等配置。在设置之前,务必保证虚拟机网络畅通。
cd /opt/ti/mcsdk_1_01_00_02/
sudo ./setup.sh
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 中的 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
#undef __ASM_ARCH_HARDWARE_H
#include <mach/hardware.h>
#undef __ASM_ARCH_HARDWARE_H
#include <mach/hardware.h>
#undef __ASM_ARCH_HARDWARE_H
#include <mach/hardware.h>
cd /opt/ti/syslink_2_21_01_05
make syslink
make samples
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的双核例程
这一部分很多都是常识性的知识,我们只整理一部分知识。
Linux的共享库里面是有构造和析构函数的,构造函数和析构函数分别是在库的加载和销毁的时候被系统调用的,这里面分为两种情况:
这两种情况的析构函数和构造函数调用的位置是不同的。第一种方法,在load二进制elf文件的时候,动态库被加载之后,就会调用构造函数,析构函数会在elf生命期结束之后调用。第二种方法,在dlopen的时候会调用构造函数,而dlclose的时候会调用析构函数。
构造函数使用方法:
void __attribute__((constructor(1))) init_function(void) {}
析构函数使用方法:
void __attribute__((destructor(5))) deinit_function(void) {}
在函数内实现这两个函数就可以了,函数名字可以自定,不可以少的是attribute,后面1和5数字代表着优先级。
我们准备一个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
准备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
我们以后编写共享库的时候记得完善析构函数和构造函数。
正常情况下编译出来的共享库和执行文件里面带有符号信息和调试信息,这些信息调试的时候非常有用,但是对于最终版来说是没有任何作用的,而且使得文件很大。使用strip的工具可以清除这些信息。
$ strip libprov.so
Note 在MACOX上使用strip 需要 -x1。
或者在ld的时候使用-S参数(-s消除所有符号信息,-S消除调试信息)来表示清除调试信息。或者使用gcc中通过"-Wl,-s"和"-Wl,-S"来完成。
我们看看效果,使用objdump -t libprov.so
打印所有符号
在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版本的。如果们重新编译了低版本的库,这个时候会报错如图。
综上,我们编写库的时候要注意:
从相对于PC地址加一个立即数写入目标寄存器,Xd = PC + imm,得到偏移imm的PC地址的地址。实际上,执行的ADD/SUB 对PC地址的指令1。
ADR <Xd>, <label>
. Note, 的范围是 ±1MB。adr x1, #0xff
-> 当前PC值,加上0xff写入x1这里一直强调一个相对PC,这个相对这个词用的十分有讲究,具体参考,[1.4 ADR和LDR的陷阱](#1.4 ADR和LDR的陷阱).
ADRP首先找到PC向下4K对齐的位置(寻找4K对齐的基地址)1,然后加上给定的赋给Xd寄存器。寻找4K向下地址可以给值的低12位(2^12=4096)清零,就可以了,这个实现可以参考链接的例子2。
ADRP <Xd>, <label>
. Note, 的范围是 ±4GB。adrp x1, #0xff
-> 当前PC值->找到4k对齐的基地址->加上0xff写入x1。新建一个汇编文件,在汇编代码中定义一个my_test_data的标签
.align 3
.global my_test_data
my_test_data:
.dword 0x12345678abcdabcd
【分析】: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
修改链接文件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无法访问到这个地址。
从上面的[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的地址外运行,此时就会出现一个问题。
我们现在制造一个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-file
3强制使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。
在介绍内存独占加载和存储指令之前,先科普一下ARMv8架构里面的一个机制-独占监视器(Exclusive monitor)4 ,虽然这个这个是ARMv6比较老的架构上面的文章,但是这个原理是不变的。ARM里面有两个独占监视器,一个本地的独占监视器,还有一个是全局的独占监视器。本地的独占监视器用于监视non-shareble/shareble的地址访问,全局的独占监视器用于监视shareble的地址访问(多核,如图Cortex-A8/Cortex-R4)。LDXR指令会让监视器进入到独占状态,STXR存储只有当独占监视器还处于独占状态的时候才可以存储成功。
实际上内存独占和加载指令为操作系统的一些原子操作提供底层的技术支持,Linux内核一些atomic的访问,比如atomic_write(), atomic_set_bit()的这些原子操作在底层的指令都有涉及到内存独占。这里有个文章可以参考,spinlock上面如何应用LDXR, STXR5.
内存独占加载指令。以内存中独占exclusive的方式加载内存地址到通用寄存器。
LDXR <Xt>, [<Xn|SP{, #0}>]
ldxr x1, sp
-> 当前sp指针独占地加载到x1寄存器内存独占存储指令。
STXR <Ws>, <Xt>, [<Xn|SP{, #0}>]
stxr w0, x1, sp
-> 独占的把x1的内容写入到sp内,写入结果放在w0寄存器,w0为0表示写入成功,w0为1表示写入失败。Note, w0是一个32位的寄存器。使用汇编实现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区域,否则在某些环境会报段错误。
对于系统寄存器的访问不能像是通用寄存器一样,系统寄存器非常特殊,所以就需要特殊的指令进行访问。
MRS <Xt>,(<sestem_reg>|S<op0>_<op1>_<Cn>_<Cm>_<op2>)
MSR <pstatefiled>,#<imm>
MSR (<sestem_reg>|S<op0>_<op1>_<Cn>_<Cm>_<op2>), <Xt>
内存屏障指令DMB, DSB还有ISB指令
DMB (Data Memory Barrier)6
保证内存屏障前后的内存访问指令的执行顺序
DMB <option>|#<imm>
DSB (Data synchronization Barrier)7
任何执行都要等待DSB前面的存储访问完成
DSB <pstatefiled>,#<imm>
ISB (Instrution synchronization Barrier)8
冲洗流水线和预取buffer,才会从高速缓存或者内存中预取ISB指令之后的指令
DSB <pstatefiled>,#<imm>
异常处理还没有完成,在异常处理(一)里面算是把ARMV8a上面的一些比较基本的异常相关的知识罗列出来了,但是还没有具体的分析。在ARM的世界,中断是异常的一种。这一节的目标是利用Linux内核研究:
ARMv8的中断有两种模式可以配置,一种是传统的中断处理Legacy模式,一种是使用GIC中断管理器模式。我们这次目标是Legacy模式,下一节异常处理(三)会学习GIC中断管理。
外部设备assert一个中断线,中断控制器会产生一个IRQ使CPU进入到异常处理流程,保存一些PC->ELR(子函数返回地址),保存PSTATE状态信息,还会切入EL1异常等级(栈指针),接着就是CPU和Kernel共同完成的中断向量表,根据中断向量表的地址进入到内核的异常handler,执行内核中断上下文。
我并非研究ARM的RTL设计的人员,但是从RTL里面能给我们提供一些本质的信息。我们这里用ZYNQ设计的双核Cortex-A9为例1,看看整个中断控制器如何工作的。
中断控制器的RTL设计如图所示,我在图片上标注了中断控制器的输入输出,输入一共是两个,分别是图中的source 1和source 2,一个是外部的通过外设引脚输入进来的中断源,一个是内部的CPU产生的中断源,通过或的关系合并成FIQ和IRQ两个中断输出,中断的输出被接入到CPU0和CPU1的接口。我们在CPU上能观测到的就是异常的产生。
我们可以在PSTATE寄存器中控制IRQ和FIQ的开关。
参考一些做CortexA72的二级产商的技术手册,NXP的imx82和德州仪器TDA4VM3,并没有设计相应的legacy中断,我以树莓派的Cortex A72(x4)为例4,了解一下legacy interrupt怎么设计的,我们在异常处理(三)中研究一下不同厂商如何将GIC集成到自己的ARM处理器上的。
树莓派是这样做的,将中断线分为几类,ARM coreN(arm核心组中断),ARM_LOCAL(只有CPU能访问的中断), ARMC(CPU和GPU共享的中断),videocore(GPU的中断)和ETH_PCIe(网络PCIE中断)。
中断比较多,传统的中断寄存器就是使用串联的方法,待定寄存器三个FIQn/IRQn_PENDING1 + FIQn/IRQn_PENDING0,还有一个PENDING2,串联起来,相当于把ARMC和外设类的中断打包,共用一个位,最后组装成ARM_LOCAL FIQ/IRQ_SOURCEn寄存器。所以我们使用这些寄存器来控制和读取中断状态。
在树莓派的手册里面举了个例子,一个UART4的FIQ中断发送到ARM core3上的处理时序:
这部分应该是中断上半段处理的内容。
中断也是一种异常和异常处理(一)的处理方式一样,摘录于异常处理(一):
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的中断现场在跳转异常向量表之前,此时还没有进入到EL1,但是备份到EL1;Kernel的保护中断现场上半段,此时已经在CPU的EL1,栈已经切到了EL1的栈空间上面,保护程序逻辑的现场。
我们看一下CPU保护中断现场的工作,kernel保护中断现场放在1.4.1 top half来说。CPU发生异常的时候会自动把SP/PC/PSTATE这些数据备份到寄存器里面,但是这里有个问题,如果我们对这个数据不加以备份,当异常嵌套异常的时候,寄存器的值就会被冲走,我们只能恢复一级的异常,因此还要备份异常到相应的栈里面,当多级嵌套的异常从根节点一路路返回的时候,从栈中拿出寄存器的数据进行返回。这个好比QQ幻想里面的飞空艇仙子,龙城->长乐村->桃源村->天都,我们手里的信息只能回溯到上一站,等我们返回桃源村的时候,打开飞空艇仙子的对话框,就不知道去哪里了。因此我们每飞一个地方之后,需要在各个城市的飞空艇仙子的复印一个副本,告诉他我来自哪里,到天都之后我就知道回到桃源村,到桃源村的时候我就知道来自长乐村,最后我就能返回龙城。这就相当于中断现场的保护,在每一次中断过来之后,备份SP/PC/PSTATE到栈空间。
在entry.S中kernel_entry就是CPU保护中断现场的工作,当然处理保护中断现场,还有很多关于内存的指令,这里面先不做过多的关注。
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 |
这部分我们在Linux Kernel专题里面来讨论,在ARM64处理器这块就暂时不讨论了。
对于Legacy中断模式,我们启动系统的定时器,在Cortex-A72处理器的树莓派4b上面实现一个定时器,需要自己完成寄存器的配置,异常向量表的跳转,寄存器的备份工作。
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中断的路由信息可以注意到:
虽然几个定时器被分配到了ARMCore组,但是实际上是在ARM_LOCAL里面的寄存器处理的,这个设计真的是有点令人很迷惑。
定时器需要配置,根据常识都需要:
我们选定的是CNT定时器,也就是EL1里面的PS定时器。在手册里面,CNTP_CTL_EL0来配置,这里还有个奇怪的事情,就是PS定时器是EL1里面的,但是配置寄存器是在CNTP_CTL_EL0内配置的(我搜了一下手册,是没有CNTP_CTL_EL1的,可能配置都是EL0寄存器里面完成的)
CNTP_CTL_EL0, counter_timer physical timer control register.
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
Timervalue的初始值,这个值会递减到0的时候会触发中断,还需要在handler里面重新赋值。
#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
树莓派一共三个这样的寄存器,这个寄存器用于软件决定从ARM core接收一个FIQ的中断请求。我们关注的应该是BIT0,是cortex-A72处理器内核的PS定时器。
timer_ps0_set_value:
msr cntp_tval_el0, x0
ret
配置PSTATE上面的DAIF使能总的中断开关。
arch_enable_daif:
msr daifclr, #2
ret
arch_disable_daif:
msr daifset, #2
ret
// 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");
}
FPGA Technical Tutorials - Zynq System-on-Chip Design Overview Interrupts ↩
i.MX 8QuadMax Applications Processor Reference Manual - 3.1.2 Interrupt Interface ↩
DRA829/TDA4VM Technical Reference Manual (Rev. C) - 9.1 Interrupt Architecture ↩
ARM Cortex-A72 MPCore Processor Technical Reference Manual r0p3 - Generic Timer functional description ↩
ARM Cortex-A72 MPCore Processor Technical Reference Manual r0p3 - AArch64 Generic Timer register summary ↩
手里有一块Friendly ARM的MINI2451的板子,这周试着编译内核,然后更新一下这个板子的Linux内核,想要更新Linux Kernel 4.1版本,但是种种原因实在是没有更新成功;于是使用Friendly ARM板子提供的3.6版本的内核,但是他们的内核全都配置好了,你只需要按照常规的方法进行编译就好了,貌似不能更深入的理解内核, 后来我从kernel.org官网上下载原版内核,然后一点点的把2451这个板子需要文件移植过去,可谓是问题百出啊,也学习到了很多东西。
我们拿到全新内核的时候,一定要注意几个步骤,好像并没有几个书上由我写的这么详细的,基本上都是给一个大体的思路,但是到了真刀真枪上场的时候,真的是问题百出。以下讲围绕这几个方面进行讨论。
a) 修改顶层Makefile
b) Machine ID的处理( 在arch/arm/ 的mach中增加C文件 -> 修改Kconfig -> 修改Makefile文件 )
c) 在arch/arm/tools/mach-types 文件中增加Machine ID
Makefile一共要修改2个地方即可:
修改arch架构: ARCH ?= arm 注意,arm这几个后面不要打空格啊,要不然make的时候不识别。
注意1,CROSS_COMPILE的表述,我这里给的是全路径,而不是像书籍和网络博客上给的 arm-linux- ,这里为什么呢?因为你的电脑里面可能装了不是一个交叉编译环境的工具链,所以这里强烈推荐使用交叉编译环境的绝对路径,绝对不会出错,可以放心大胆的使用。
注意2,arm-linux- 后面不要加空格,否则不会识别的,和ARCH那个选项一样,都不要有空格。 否则就会抛出:“ make: ** /home/delvis/work/linux-3.6/arch/arm: 是一个目录。 停止。”的错误。
Makefile只需要改这两个位置就可以了,大可保存。
路径就是 -> ./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就是我们一会儿要写入machine id的宏定义,我这里宏定义的字符串是MACH_MINI2451
刚才刚刚添加了新的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的时候,最先应该的就是选择板子的架构
- 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来说都是一项配置。
看图,首先注意两个画红圈的位置,我们讲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型号,我们的这个选项才会出现
我们平白无故的增加了mach-mini2451.c文件,如果进行全局编译的时候,是不会进行编译的,因为没有makefile进行指导编译,所以我们需要修改这一层的Makefile,当我进行全局编译的时候,就有规则指导编译器进行编译。
# add by Carlos 2017.12.6
obj-$(CONFIG_MACH_MINI2451) += mach-mini2451.o mini2451-lcds.o
在这一层的Makefile文件中随便找一个位置,然后按照这个格式输出文件。
说到机器码,我真的不得不吐槽一下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值。
到此我们的的内核配置完毕了。
切换到最后一步,进行make menuconfig进行.config的配置,在韦东山老师的视频里面,提到这个.config文件来源于三个部分
- 进行make menuconfig配置后的,你需要一条条的进行配置。
- 使用默认配置,在上面修改
- 使用厂家提供的
我这里,使用厂家提供的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的问题: ->
Machine ID的处理( 在arch/arm/ 的mach中增加C文件 -> 修改Kconfig -> 修改Makefile文件 )
在arch/arm/tools/mach-types 文件中增加Machine ID
在make menuconfig的menu中选择system type,选择正确的型号,会自动配置到.config
然后开始make就可以了,编译内核,我一般都喜欢用make -j8 多线程编程,速度快,但是很烧CPU。。。
基本上都是这样的状态。。。看温度。
编译完成之后,按照Friendly ARM提供的文档,把zImage文件拷到SD卡正确的路径,然后从SD卡启动正常烧写就好了。
看到这样的输出的时候,真的很激动,终于内核开始正常解压了。磨了人好几天。
第15周主要搞内核的搞完了,最近要忙着复习考试,内核事情可能要稍微缓缓了。以后再慢慢研究。
[1] 贵气的博客著.Error: unrecognized/unsupported machine ID (r1 = 0x33f60264)..新浪博客.2011-03-20.
[2]韦东山著.《嵌入式Linux完全开发手册-应用开发》书籍.
[3]FriendlyARM著.2451开发手册. 用户手册.
1. 本文为MULTIBEANS团队研发跟随文章,未经允许不得转载。
2· 文中涉及的内容若有侵权行为,请与本人联系,本人会及时删除。
3· 尊重成果,本文将用的参考文献全部给出,向无私的工程师,爱好者致敬。
前面两节是对单独的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
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;
}
编译: 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
使用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
链接器的作用就是对几个目标文件合并成一个输出文件,从上面的例子中可以看出,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段也类似。这里需要注意的是:
探究一下合并之后段发生变化的过程:
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.
总结下来:
在06_ARMv8_指令集_一些重要的指令 的1.4部分,可以看到嵌入式系统里面LMA和VMA不一样的例子,ARMv8的LDR指令和ADRP指令加载的地址上对VMA和LMA由非常明显的区别。
对于注意2,在《程序员的自我修养》一书中p103提到:
在Linux下,ELF可执行文件默认从地址0x08048000开始分配。
对于这个设定,在网上也能找到相关资料2,但是在x86的体系架构中和arm体系架构中对文件进行编译,得到的.data段却有着不同的结果,是从0x00400000开始分配的。这个似乎和书上讲的不一致。本文先留个悬念,后面到虚拟内存映射再回来解答这个问题。
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函数
上面需要调整的地方有个重定位表(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没有被定义。
如果有全局未初始化的变量,该符号会被认为是弱符号,同样的通过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;
}
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
int c_3 __attribute__((nocommon))
如果不以common形式处理,那么在链接的时候遇到相同的符号,就会发生编译报错。
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
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;
}
}
aarch64-linux-gnu-gcc -c e.c d.c
aarch64-linux-gnu-ar crv ed.a e.o d.o
flowchart LR
C[de.a]-->|ar -t|B[d.o]
C[de.a]-->|ar -t|F[e.o]
aarch64-linux-gnu-ar -t ed.a
两个方法:
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
内建函数,顾名思义,就是编译器内部实现的函数。这些函数跟关键字一样,可以直接使用,无须像标准库函数那样,要 #include 对应的头文件才能使用。内建函数的函数命名,通常以 __builtin 开头。这些函数主要在编译器内部使用,主要是为编译器服务的。内建函数的主要用途如下。
aarch64-linux-gnu-gcc f.c ed.a -I ./ -L ./ -o f.elf -fno-builtin
进入Linux的世界,发现真的是无比的有趣,也发现搞Linux驱动从底层嵌入式搞起真的是很有益处。我们在单片机、DSP这些无操作系统的裸机中学习了这些最基本的驱动,然后用过GPIO时序去模拟、然后用那个芯片平台的外设去配置参数,到Linux的世界,对于底层的时序心中有数,做起来就容易很多。学习的过程就是不断的给自己出难题,然后去解决他,在未来工程里面遇到这个问题,就瞬间可以解决了,这就是经验的积累吧。
Linux驱动目录,包含了底层写好的SPI驱动,我们需要想办法调用人家写好的SPI驱动,就不需要写IO口模拟SPI时序了。在网络上,对于SPI应用级的驱动倒是很多,平台级驱动很少,而我们想把平台级驱动二次包装在我们的字符设备驱动中,对于用户,无需考虑SPI通信写协议还是写命令,只需要使用read和write函数写显示的内容就好了。
基于这样的想法,我们找了一个使用SPI协议的从器件来实现,我手里面有OLED设备,是支持SPI协议在OLED显示面板上显示字符的。所以搭建一个实验平台,做一个OLED的demo,未来所有的从SPI设备都遵循这个框架(而且我们在这个驱动中加入 了内核机制的驱动的自旋锁、互斥体的内核操作)。
实验平台如下:
总体驱动架构模型如图所示,对于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操作。
Linux Drivers目录具备一定的通用性也具备各个架构区别不同,在包含头文件的时候,要包含
#include <linux/spi/spi.h>
#include <mach/slsi-spi.h>
同时也要关注:
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;
这个语句了。
在驱动里面对于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函数就好了。
探索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日
内联汇编并非ARCH64专门的使用方法,而是GNU编译器通用的做法。目的有二,其一,对于时间敏感的函数使用内联汇编减少执行开销;其二,C语言无法访问架构级的特殊指令,比如内存屏障功能。
Define:asm ("asm instruction")
【示例】:
asm(icicalluis)
调用一条高速缓存维护指令。
Define:asm qualifier-asm (AssemblerInstruction)
Instruction:
限定词:volatile
,inline
【示例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"
);
}
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.(输入参数的指令执行完成之后才能写入) |
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"
);
}
以下需要注意:
再做一个例子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"
);
}
内联汇编也可以和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)
最后宏展开为几个函数:
这三个函数只是在内联汇编里面 "#asm_ops"位置不同。
[Abstract]: 在Qt on Linux上开发程序完成后需要脱离本机所营造的开发库环境变量运行,则需要对二进制执行文件进行打包发布。在Ubuntu系统中通常使用deb格式文件对二进制安装包进行封装。Ubuntu系统提供了个人软件包(Personal Package Archives, PPA)在线安装deb功能。本文阐述在Linux系统中使用Qt编译生成的二进制文件收集*.so库文件过程、Ubuntu的deb打包过程,完成从Qt编译生成的二进制文件到发布到Ubuntu个人软件包文档全过程。
使用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打包,需要先创建符合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 ....
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.
在postinst内可以写一些处理快捷方式、配置环境变量的操作。
# !/bin/sh
cp /opt/tinyserial/tinyserial.desktop /usr/share/applications
cp /opt/tinyserial/tinyserial /usr/bin
笔者会做两个处理:
/usr/share/applications
/usr/bin
中与postinst回滚执行的文件。
# !/bin/sh
rm /usr/share/applications/tinyserial.desktop
rm /usr/bin/tinyserial
dpkg -b tinyserial tinyserial-v1.1.deb
安装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
追火车. 【Qt依赖库】Linux 环境下 Qt 可执行程序依赖库打包脚本. ↩
新月时刻. dpkg制作deb包. ↩ ↩2
最近有个接了一个小型项目,内容很简单,就是解析北斗GPS的串口数据然后输出经纬度,但接过来觉得太简单,就发挥了主观能动性,增加了百度地图API,不但能实时定位,还能在地图上标识出位置信息,用的QT5.5。上位机运行图片如图所示:整体运行比较流畅。
原理就是界面上集成一个WebKits/WebView,让Qt和Javascript进行交互。但需要注意Qt5.6以上版本取消了WebView的模块,换成了webenginewidgets,看上去配置好麻烦,甚至还要自己编译什么的,虽然性能可能有指数性的提升,但对于我这个做嵌入式软件和硬件,上位机会个基础的就算是很好的人来说,还是webkits好一点。
2017/07/22 更新:
在本文后续版本已经适配了Qt 5.6 以上版本的QWebEngine版本,摒弃了QWebKits组件,后续若有经费的话,将继续更新支持QWebChannel通信通道。两个版本都可以在本文尾部的附件中下载,欢迎学习讨论。
本设计开发主要涉及三个方面:
串口开发不用说了,请参考我前几篇有个蓝牙的博客,上面有源码,Qt on Android 蓝牙开发,本设计中的串口部分就是基于那个串口开发的。串口开发自动检测连接设备,不需要进入管理器和找到COM口是多少,自动和串口进行连接。
串口数据粘包和数据不连续很头疼,进入一个串口接收槽函数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;
}
}
百度地图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);
}
做完这个程序之后呢,我开始研究如何给程序打包,终于在两个小时内搞定了。
Step1:把所需要的dll文件集成出来。
Step2:用工具把dll文件exe文件和其他文件封装起来,做成msi或者exe文件。
工具就是一个Qt自带的windeployqt工具,另一个是Advanced installer安装包打包程序。
1)在开始菜单找到Qt文件夹,里面有个像是cmd命令行一样的东西,我的是MinGW的。反正运行出来这个样子:
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文件和其他支持库文件都放在这个文件夹了。实际上到了这步你可以打成压缩包然后发布到任何电脑,解压直接运行
我还是比较喜欢把他弄成安装包,这样更方便,更正式。Advanced Installer这个工具简直太赞了,打包的程序安装界面十分的正式,一点都不山寨,还有很多安装包的皮肤可供选择。如图为打好包的图标:
运行后的效果如图:
里面还提供了导入注册表、安装后创建快捷方式等等方便的配置。
** 下载地址: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· 尊重成果,本文将用的参考文献全部给出,向无私的工程师,爱好者致敬。
曾经在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 |
GIC-400在SoC上的位置如图所示,GIC-400的输入是一对中断信号,还有一个可编程的接口,输出是一堆FIQ和IRQ。简言之,GIC-400就是对一堆中断信号最后整理成FIQ和IRQ的系统。从一个系统角度而言,GIC-400没有对输入进行同步化处理,因此在做一些时序比较强的场景时候,输入信号(SPI PPI)必须和CLK进行同步。
中断类型总结为下表:
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 |
这里面一个共5个部件,distributor和cpu interface是关注的重点。
其他一些部件是关于虚拟化的,暂时这里不涉及。
在GIC内部,中断信号有以下几种状态:
Note1, PE是processor element, 这里视为cpu core
Note2, 被PE识别的意思是,Receives acknowledge and EOI accesses from that processor
外设中断的触发类型与状态机转换的问题:
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
GIC-400提供的中断时序图,设定N中断优先级高于M中断,这个是一个电平触发的中断时序图:
ARM提供了一些GIC-V2的通用寄存器:
分发器控制寄存器必须被配置用于使能中断组和设定路由模式:
CPU Interface Configuration
PE配置
routing controls
SCR_EL3和HCR_EL1,这个路由控制位决定中断发生在哪个异常等级,必须被软件初始化。
interrupt masks
PE必须异常mask位在PSTATE。
vector table
VBAR_ELn寄存器必须配置。
SPI,PPI和SGI配置
使用分发器的GICD_*配置SPIs
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.
为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.
bcm2711集成了GIC-400,基于GIC-400架构。GIC-400中断号的分配和SoC设计有关。
如果在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,参考引用文献。
这个是nxp imx.8使用GIC的设计,在SoC的结构设计上,通常自己会堆叠一些逻辑,然后作为外围中断的输入到GIC,NXP还设计了IRQ Steer,由于是大小核设计,这里面不只是同一个架构的core,还包含了中断的多路复用器,使用二级厂商的芯片除了要注意配置ARM原生的GIC之外,还要注意配置二级厂商设计的RTL模块的配置,及这些模块和GIC之间的一些耦合关系。
TI设计的也类似,里面增加更复杂的总线仲裁机制和逻辑判断。现在我们也知道了,这些厂商拿公版的ARM做了什么处理。
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中进行展开讨论。
FFTW是一個做頻譜非常實用的組件,本文講述在Windows和Linux兩個平臺使用FFTW組件。Windows下的的FFTW組件已經編譯好成爲dll文件,按照開發應用的位數下載好組件包後直接按照dll規則使用組件;Linux下則需要自己進行編譯。
**FFTW源碼包的下載:**http://www.fftw.org/download.html
在終端輸入:
./configure --enable-type-prefix --prefix=/usr/local/fftw --with-gcc --disable-fortran --enable-i386-hacks --enable-shared=yes
常見錯誤: 提示--enable-type-prefix沒有找到文件,此時請檢查上面命令每個選項之間的空格和縮進是否混淆,全部更改爲空格。
make -j8
make install
make clean
./configure --enable-float --enable-type-prefix --prefix=/usr/local/fftw --with-gcc --disable-fortran --enable-i386-hacks --enable-shared=yes
make -j8
make install
最後在/usr/local/fftw路徑中又so、a文件。
下载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
2017年的十一假期,足不出户,一个人在教研室里面搞Qt的移植。我手里面有Samsung的CortexA8,Samsung的 CortexA53还有Ti的Sitara系列的AM3517的ARM,都成功的移植了Qt,然而在我接触ZYNQ这个平台的时候,偏偏搞的我三天的时间,无法移植,ZYNQ上面安装的是Linaro这个阉割版本的Ubuntu,怎么都不成功,一个问题,在我的PC机上编译完到Linaro上面运行的时候,没有反应,不启动也不报错。
你需要用嵌入式环境编译Qt和Qt所开发的程序。
我的环境:
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
对触摸屏信号的获取、校正、滤波处理,均采用开源的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成功。
将下载的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
/****************************************************************************
**
** 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 拷贝到开发板的文件系统中对应的目录中。
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.
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.
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:
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-----
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. **
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.
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;
}
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;
}
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:
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;
}
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.
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;
}
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");
}
}
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.
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;
}
/*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)
/*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;
}
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;
}
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;
}
You get the driver code and testcase from my github as follows:
https://github.com/carloscn/cryptography
That is a Clion format project.
[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/
这个稍稍微微有点操作系统里面的东西了,《程序员的自我修养》一本书里面,是站在可执行程序里面看的操作系统,比如说程序是分页管理的方式,大概说明了一下分页什么原理,而从《操作系统原理》一本书里面是站在操作系统的角度去看可执行程序,就会具体的讲操作系统是如何分页的,比如hash页表,页偏移具体的公式。两部分可以说是完全不一样的视角,本文意图就是希望结合两个视角整理出一份自己理解的笔记。
早期程序在ROM中直接加载到RAM里面执行,这种方法就十分简陋;RAM是一个十分珍贵的资源,这种方式当然不能被接受,后来人们提出了Overlay覆盖装入的方法,但这样的方法需要程序员自己对互不相关没有调用的函数进行管理,需要根据依赖关系组织成树状结构,对于开发者十分不友好;现代操作系统使用也映射(paging)的方法,按页完成数据和指令在ROM和RAM中的换入和换出(SWAP);现在操作系统还引入了MMU,让这种paging变得更为复杂。
最终程序的执行从编译出的ELF逻辑空间,会被映射到PVS(进程虚拟空间),最后被MMU映射到PMS(物理内存空间)。我们在编译一个文件的时候在ELF文件中readelf可以看到VMA的地址,VMA的地址是虚拟内存区域(Virtual Memory Area)。关于ELF->PVS,我们这里关注点在于ELF->PVS加载的过程,对于PVS->PMS属于MMU的知识范畴,这里暂时不涉及,相关TOPICS也会在ARM架构和Linux内核里面着重展开。
进程在开始初始化的时候读取可执行文件头,并且建立虚拟空间和可执行文件的映射关系。(ELF -> PVS)这个过程我们可以叫做映像文件加载(image loading)。
我们在开头的时候说,线代操作系统采用paging的方法,既然是节约RAM的方法,必然是从存储在ROM中的ELF部分页映射到PVS上面。这里其实有几个很简单的concern:
文献1提到swap技术中的两种方式,一种是是标准交换,一种是移动系统的交换。文献2提供了多个页面置换的算法供操作系统选择。而当进程执行的时候发现指令数据没有被映射,那么就会发生页错误(Page Fault),此时就会激发缺页中断,CPU强制抢占进程执行,在缺页中断的下半段,在不同的系统中就会采用不同的页面置换算法,当中断返回之后进程继续执行,在PVS内就有对应的PMS了。所以,页错误也是评价一个页面置换算法的好坏的指标,为了追求更好的性能,尽可能的减少页错误的发生。(文献1给了一组关于系统切换上下文的数据,100MB的进程,传输速度为50MB/s,100MB进程换入和换出的时间需要4s。)
页错误的一种场景3:
进程会选择性映射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的处理流程。
这里和生活中一些我们在应用观察的现象做一个引申。文献4提到苹果的ISO内存管理一些比较初级的原理,我们在使用ISO的时候不会担心RAM被吃光,甚至库克不建议清理ISO后台。我相信一部分原因也是因为没有采用swap技术,当然也进程管理及低功耗管理肯定有着很大的作用,从文献4摘录:
iOS
中,内存分为两种,一种为Clean memory
,另一种为Dirty memory
;
Clean memory
的page
可以换出,既磁盘中有其对应内容,系统可以在内存紧张时将Clean memory
的page
换出,当再次访问时,可以重新从磁盘中读取,我们使用的图片、mapped files
、Framework
的数据段常量以及代码段等,这些都是Clean memory
。
Dirty memory
是无法换出的,我们所有的堆上的分配等都是属于Dirty memory
,所以我们一定要尽可能的减少Dirty memory
的使用。
而Linux使用这项技术,我们在安装UBUNTU分盘的时候,常常需要预留2GB的SWAP空间,实际上就是在这里应用。那么应用SWAP的切换进程上下文,还有双缓冲都会吃掉一些性能。
如果我们自己定义了很多段,甚至一个变量占了一个段,那么VMA在映射的时候就很肉痛了,因为ELF文件被映射的时候是以页为单位的(意味着映射长度是系统页单位的数倍,Linux系统页可能是4KB/8KB/16KB,利用getpagesize()或者命令行getconf PAGESIZE
)。如果定义一个变量独占一个段,那么可能在VMA上面占据4KB的长度。在VMA角度不会关心你自己定义的这些段5,VMA角度只关心这些段的可读可写可执行的属性还有逻辑地址的值。因此,在装载过程中:对于权限相同的段进行合并之后再映射。这里有个约定俗成的叫法,我们把没有之前的段叫做section,把合并之后的段叫做Segment5,当我们写gcc程序的时候出现Segment fault这种错误的时候,就可以知道是访问内存权限出错了。
查看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):
我们可以从视图的角度定义section和segment,section是链接视图里面的概念,而segment是执行视图里面的概念。
我们程序内部肯定是用到堆(Heap)和栈(Stack)了,这部分在elf上面怎么表述的,尤其是使用malloc从堆分配空间这是一个动态的过程?malloc肯定是从PVS上面分配的VMA,那么如何表示,难道编译器可以预测malloc的行为?
我们在IMX6ULL的嵌入式Linux上面查看test_msg_1.elf进程的虚拟空间分布,cat /proc/pid/maps
如图所示:
图中第一列就是VMA的范围,第二列权限,第三列偏移,第四列主设备和次设备号,第五列映像文件的节点号,最后是文件路径。这个程序里面有很多动态链接库的地址,还有一些用[]标注起来的地址,他们被称为匿名虚拟内存区域(Anoymous Virtual Memory Area)
[heap] : 136KB大小
[stack] : 132KB大小
[vdso] : 该地址已经位于内核空间,用于和内核进行一些通信,暂时不在本文展开。
VMA | 说明 |
---|---|
代码VMA | RW,有映像文件 |
数据VMA | RWX,有映像文件 |
堆VMA | RWX,无影响文件,匿名,可向上扩展 |
栈VMA | RW,无影响文件,匿名,可向下扩展 |
这里多说一个stack和heap有趣的知识,stack和heap的增长方向,stack的增长方向是向下增长的(高地址->低地址),而heap的增长方向是(低地址到高地址)6。这个原因就是,STACK和HEAP是动态区域,很难划分出界限,所以让两个区域朝着一个方向滚动,可以充分利用各自的空间。
还有一个需要注意的是,Linux装载ELF的时候并直接映射,比如上图.RW区域头地址并非对应DATA区域的头地址。linux内核里面有个“hack”的方法,简言之就是上个区域没用完的段会被插入一些其他的段__libcfreeres_ptrs,这种做法在Linux内核的elf_map(), laod_elf_interp()中。
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;
}
我们装载以段为基本单位划分,使得内存的利用率提升了,但是我们总是要一直追求内存的利用率不断提升的,比我们继续分析,假如有三个段,大小分别为127,9899,1988,那么他们分别需要1,3,1个页,一共需要5个页,但是每个页都存在页内碎片,尤其是第一个段,仅仅127的大小,却要给其分配一整个页,三个段大小一共是12014字节,但是却需要20480字节的空间,空间使用率只有58.6%,所以我们继续思考如何提高内存利用率。
一般大部分unix系统采用的都是相邻页合并的方法,注意 : 我们采用相邻页合并,改变的是物理内存的分页方式,虚拟内存中我们依然是按照正常方式分配页,比如上例中,我们采用相邻页合并,会使得分配的物理页数目减少,但是虚拟内存还是按照正常分配方式分配5个,只不过会存在虚拟内存中两个页映射到同一个物理内存页上,比如,让第一段的唯一一个页和相邻段(该段有3个页)的相邻页合并成为一个页,如下图
左图是没有分段的情况,SEG0和SEG2单独使用一个页,SEG1由于比较庞大使用了三个页,因此用了5个页。而右边的图中对ELF进行页大小分割,在PMS上面,page1包含了SEG2和部分SEG1,page2被SEG1的部分独占,page3包含了SEG0,ELF头和SEG1的部分,最后依靠MMU把PMS共享的部分展开到PVS上面。相邻页合并使页的使用率提高。
我相信计算机发展的今天,很多设计都是为了弥补一些场景的漏洞,而这些漏洞作为年轻一代的我们并没有参与过,不过读读相关论文和博客资料能从中挖掘一点乐趣,也能了解一下计算机专业的同学到底在研究什么。1988年的时候莫里斯李用unix操作系统fingerd软件中缓冲溢出安全漏洞写出了莫里斯蠕虫,基于缓冲区(堆栈)溢出的原理。基于缓冲区的攻击包括栈溢出、堆溢出、整形溢出、格式化字符串攻击、双重free8,基于植入性的攻击包括代码植入攻击还有return-into-libc攻击。
怎么攻击?我们可以看到没有开启ASLR的动态库是固定值,黑客如果想要攻击系统可以修改这个固定值加载的动态链接:栈溢出或者return-into-libc来实现攻击。攻击者可以通过缓冲区溢出改写返回地址为一个自己实现的库函数地址,并将库函数执行的参数也重新写入栈,这样函数调用时获取的是攻击者的设定好的参数,并且结束之后返回到函数而不是main,具体操作过程可以参考9,里面通过栈溢出操作获取root权限(编译器关闭–fno-stack-protector
)。
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爆破。
Canary这种方法只局限于对栈的保护。空间地址随机化是通过对操作系统内核或者C库进行修改。使得进程加载到内存地址是随机化,从而降低攻击成功的概率。在return-to-libc或者.plt/.got覆盖场景下,攻击者必须知道指定地址。地址空间随机化之后,指定地址难以确定,达到抗攻击的可能。地址空间随机化包含四个层面12:
-fpie
选项这里就不具体解析了,参考文献12。
在Linux Userspace中对ASLR的使用7:
地址空间随机化
ASLR(Address Space Layout Randomization)
查看当前系统的ASLR配置情况![]()
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![]()
使用ldd命令可以观察程序所依赖动态加载模块的地址空间,当ASLR开启时,地址就会发生变化
终于,我们从ARMv8的一些汇编指令的学习当中出来了,开始研究ARMv8体系架构的设计。这里面的机制十分的重要,所以不得不依赖笨叔的视频,同样自己也要阅读ARMv8的手册,希望自己一字一句的去理解,不懂的去搜索,争取把这部分知识补充完整。
这部分在笨叔的视频和讲义里面只是粗略的讲了一下high-level的版图设计,但我觉得这个十分重要,没有这个基础后面很难去理解异常处理的过程,而且在high-level上面,人为的设定了很多规则,我觉得有必要整理一下。大纲总结如下:
这里主要提到了一些概念和术语,我们需要理解这些概念还背后术语的一些潜在的设计意义,挖掘出设计者的意图。文档在这一章开篇就提到:异常等级(Exception level)、特权等级(Privilege level)。ARMv8内部包含4个异常等级:
这里就有些需要注意的了,特权等级是在ARMv7里面的概念,ARMv8属于借鉴过来,PLn对应ELn。
ARMv8提供了两个安全状态(Secure States),一个叫普通世界(normal world),一个叫安全世界(secure world),这两个世界并行的运行在同一个硬件设备上面,安全世界的工作重心是抵抗软件和硬件的攻击。在EL3下的secure monitor,游走于secure world和normal world。
在normal world的hypervisor(VMM)代码运行在系统上并且管理着多个Guest OS,所以每一个操作系统都运行在VMM上面,每个操作系统在同一时间都不知道有其他操作系统在运行。
Guest OS kernels,这部分分为两类:
Hypervisor,总是normal world的,when present,主要是给rich os kernels提供服务。
Secure Firmware,这段程序必须运行在boot时间,用于初始化trusted os,平台,还有secure monitor。
Trusted OS,在EL1和Guest OS并行运行,提供一个runtime环境执行安全应用。OPTEE-OS
ARMv8定义了两个执行状态,aarch64和aarch32。这两个执行状态和异常等级没有概念上面的交织,也就是aarch64和aarch32都有相应的异常等级和特权模式。在aarch64执行状态时,使用A64指令,在aarch32执行状态,使用A32或者T32(Thumb)指令集。
AArch32模式,有一点不同的是关于trust-os的位置,trusted-os软件执行在secure EL3,在aarch64执行模式,是执行在EL1中的。ARMv7和aarch32很像。
对于异常模式的变迁,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处理器中异常模式有以下规则:
ERET
指令ERET
之后只能保留在本层或者是更低的层级。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的并没有这个能力。
在ARM的世界我们应该纠正一个概念,“中断”。通用的中断的定义就是可以阻止正常软件流程执行的中断。然而在ARM的术语里面,不能仅仅用中断来表示中断,我们需要更上一层的定义,异常(Exception)。异常包含狭义的中断,也包含条件变量(conditions)或者是系统事件(system event),这些行为都有专门的异常处理函数来处理。在ARCH64里面有三类异常,中断(interrupts),中止(aborts) 和复位(reset)。
在AArch64体系架构中有两种类型的中断,IRQ和FIQ。FIQ的优先级高于IRQ,这部分中断设计的时候我们再说。即便我们对于中断的概念已经很熟悉了,但这里面需要澄清几个有关中断的概念。
对于触发中断,在CPU上有专门的动词术语,assert一个中断,take一个异常。中断线由外部外设assert,中断线的信号被接入到中断控制器,中断控制器根据中断优先级判断先发出哪些信号,再根据中断向量表查找到中断处理函数的地址。由于中断并非执行在给定时间内,因此中断属于异步异常。
同样包含两列aborts,取指失败(Instruction Aborts)或者是访问数据失败(Data Aborts)。
在内存上,发生这种异常的场景可能有两种,在访问内存时,外部存储器返回一个错误,或者指定访问的地址没有关联到真实的内存中(MMU产生该错误,一个操作系统可以使用MMU abort动态的分配内存给应用程序)。
在指令上,aborts发生在取指还未执行的时候。data abort发生在存储和加载指令。
abort异常属于同步异常。
reset异常有着最高的异常等级。当异常发生的时候ARM处理器就会跳转到指令所在的位置。RVBAR_ELn
包含跳转地址,n应该是该处理器的最高权限,armv8里面就应该为n=3。该异常不可屏蔽,不可禁止,并且尝尝执行在上电后的初始化的时候。
一些异常需要由CPU外部的触发中断线间接性的塑造异常,一些系统调用指令也可以直接的产生异常,这些指令通常会向处理器请求一些运行在更高权限环境中的服务。
有几个需要注意的点:
HCR_EL2.TGE
被置位的时候,这个时候异常会发生在EL2。我们要注意一个比较重要的概念,中断嵌套(中断抢占),从定义上来看4:
中断嵌套指中断系统正在执行一个中断服务L时,有另一个优先级更高的中断H触发,这时中断系统会暂时中止当前正在执行低优先级中断服务程序L,而去处理高级别中断H,待处理完毕,再返回被中断了的中断服务程序L继续执行。
从处理器的角度来看,ARM64是支持中断嵌套的,我们可以从三个角度来看5:
首先来看ARM CORE支持中断嵌套吗?答案 支持! 但是是有一个前提,在进入中断处理时,PSTATE的I、F、A等比特位是MASK的,软件中需要主动unmask后,那么就可以中断嵌套了。如下图所示,正是ARM Core支持中断嵌套的一个示例(或者叫模型):
每次调用要保存中断的上下文。
继续看GIC中断控制器支持中断嵌套吗?答案 支持中断抢占,支持中断嵌套。介绍以下优先级和抢占的概念:
GICD_IPRIORITYn
或 GICR_IPRIORITYn
表示),它是一个 8 位无符号值。 0x00 是可能的最高优先级,0xFF 是可能的最低优先级。ICC_PMR_EL1
)。 该寄存器设置将中断转发到该 PE 所需的最低优先级。 只有优先级高于寄存器值的中断才会发送给 PEGIC中断控制器的中断嵌套是可以配置的。分为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.
继续看Linux Kernel操作系统有没有使用中断嵌套?答案 没有使用!
首先查看ICC_BPRn_EL1
寄存器的配置:
https://elixir.bootlin.com/linux/v5.13.19/source/drivers/irqchip/irq-gic-v3.c#L1005
写入的是0,也就是意味着,N=0, 即下图的第一行,也就是说抢占是开启的。
继续看,针对每一个 INTID(中断号) 的priority的配置,如下所示,在gic初始化阶段,给每一个 INTID(中断号) 都配置成了一样的优先级,值位0XA5。 也就是所有中断的优先级都是一样的。
https://elixir.bootlin.com/linux/v5.13.19/source/drivers/irqchip/irq-gic-v3.c#L803
事实上,在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
目前比较流行的几种嵌入式实时操作系统有uC/OS、RT-Thread、FreeRTOS等,对外都宣称它们是嵌入式实时操作系统,那么什么叫实时呢?所谓【实时】 其实就是【及时】,能够及时的处理各种任务和中断,而如何实现【实时/及时】呢,本质上就是要支持:高优先级任务/中断能够抢占低优先级任务/中断,这里的【抢占】本质上就是【中断嵌套】。
关于RTOS系统如何驱动CPU进行中断嵌套,参考78。内部使用了就绪链表的结构体,这部分暂时不在ARMv8中展开讨论。
我们在做加减乘除的指令的时候从PSTATE
寄存器的高4位读取NZCV的值,来确定状态。今天要涉及的是PSTATE
DAIF的值,这部分是异常处理标志位。根据手册的翻译,如果异常发生,PSTATE信息会被存入到Saved program status register(SPSR_ELn),只有3个,SPSR_EL1, SPSR_EL2, SPSR_EL3。
DAIF就是exception bit mask bits,还有SPSel
当异常发生的时候,PSTATE寄存器的值(current EL, DAIF, NZCV etc)都会被复制到SPSR_ELn,返回的地址会被也会被存储到ELR_ELn中。
有几点需要注意:
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
异步和同步异常在ARM64处理器里面处理的方式不太一样,而且使用的寄存器还有寄存器的行为都是不一样的。这一小节笔记主要就是来分析和记录一下同步异步处理的不同点,后面还有关于aarch32和aarch64混合的时候,arm64的一些行为。ARM的手册在最开始的时候就强调一个事情,return address是否包含具体的发生异常的原因。这个有什么用不太清楚,手册里面注明,对于同步的异常,回退地址总能包含发生异常的原因,然而对于异步的异常,回退地址可能会包含发生异常的原因。
异步的异常主要有三种:
同步的异常包含:
同步异常的触发有很多种情况,这里需要注意的是,异常不代表错误,异常状态也可能是系统正常处理的一部分,比如,在Linux里面内存缺页错误,还有我们之前说预计加载指令的page fault10,这都是为了提升性能必须要制造的异常场景。同步的异常处理包含:
在ARMv7架构里面,预取指令prefetch,数据错误Data abort,未定义异常undef是分开的指令。而在AARCH64架构里面,都视为同步异常,如果我们想在AARCH64体系架构里面知道具体的异常是什么东西,那就需要配合其他寄存器来获取更全面的信息,ESR_ELn,FAR_ELn。
有个AARCH32和AARCH64混合的场景,我们在运行32位程序的时候需要进入到EL1执行,这个时候32位的程序发生了异常,需要进入到EL2处理,此时EL2还是AARCH64的,这个时候我们想要获取到32位程序的异常,需要进入到EL2的FAR寄存器读取信息,EL1的AARCH32的执行状态的FAR,全部清0。
对于比较高等级的EL2(hypervisor),EL3(secure monitor):
执行应用程序想要使用特权指令这个时候就需要系统调用。其中的一种方法就是SVC指令。当应用程序call这个SVC的时候,就会产生异常,处理器理所应当的进入到了更高一级的EL。这个时候如果我们想传递一些参数,就通过写寄存器的方式。
如果在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通过SMC指令。如果在EL2/3 向后call HVC指令,也会有个同步异常发生在本异常等级。
官方手册里面给了一个图表示EL0 如何 call进入EL1的,可以说这个是一个直接关系,EL0是没有任何异常处理机制的。
我们补充一下关于EL1 call进EL2/3的图,对于同步的异常而言,没什么好说的了,就是一级一级的call进去,但是对于异步的异常而言,就有点不一样了,我们EL1需要call EL2或者EL3的时候,需要配置HCR寄存器决定路由信息,同样的EL3也是需要路由信息的配置的。否则会产生新的异常。
还有一个比较特殊的情况,当有一个从AARCH32发生的异常到AARCH64处理,必须做一些特殊的考虑,AARCH64需要访问32位的寄存器了,直接访问肯定会hang死,所以ARM处理做了一些映射处理:
HCR_EL2.RW记录了EL1运行在哪个模式, 1代表aarch64, 0是aarch3211
关于返回的时候,也是通过寄存器找返回地址和返回的模式,在SPSR寄存器M[4:0]记录了返回的模式。
采用基地址+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。
以下是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实现。
异常向量表 inv_entry 1 , BAD_FIQ
,指定el为1,备份sp到x0,把esr_el1(存储具体异常原因的)被分到x2接着跳转到bad_mode里面。
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;
};
上面罗列了很多异常元素的概念,现在需要有机的将上面的内容串起来,看一下ARMv8到底如何处理的异常。CPU需要做的事情:
操作系统需要做的事情:
在操作系统执行ERET指令,CPU需要从ELR_ELx寄存器中恢复PC的指针;从SPSR_ELx寄存器恢复处理器状态。
基于树莓派和QEMU,上电跳转的时候benos处于EL2下,请把benos切换到EL1中运行。
【提示】:
【程序流】:
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]
要判断当前的是EL几,可以通过访问CurrentEL
12系统寄存器来确定,这个是个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
这个HCR_EL2
11主要用于设定我们的目标异常等级。
主要是配置这个:
#define HCR_EL2_REG_SET_AARCH64 (1 << 31)
ldr x0, =HCR_EL2_REG_SET_AARCH64
msr hcr_els, x0
SCTLR_EL1
13EL1的系统控制寄存器上面可以关掉MMU。
#define SCTLR_EL1_REG_CLEAR_MMU (0)
ldr x0, =SCTLR_EL1_REG_CLEAR_MMU
msr sctrl_el1, x0
SPSR_EL2寄存器14
#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
ELR_EL2
15设定异常返回地址。
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
// 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());
}
}
[1]: 2022-8-13: 增加 2.3章节,解说中断嵌套机制
ARM Cortex-A Series Programmer's Guide for ARMv8-A - Fundamentals of ARMv8 ↩
ARM Cortex-A Series Programmer's Guide for ARMv7-A - ARM Processor Modes and Registers ↩
ARM Cortex-A Series Programmer's Guide for ARMv8-A - AArch64 Exception Handling ↩
Arm Cortex‑A78AE Core Technical Reference Manual - HCR_EL2, Hypervisor Configuration Register, EL2 ↩ ↩2
Arm A-profile Architecture Registers - CurrentEL, Current Exception Level ↩
Arm Armv8-A Architecture Registers - SCTLR_EL1, System Control Register (EL1) ↩
Arm Armv8-A Architecture Registers - SPSR_EL2, Saved Program Status Register (EL2) ↩
Arm A-profile Architecture Registers - ELR_EL2, Exception Link Register (EL2) ↩
手里有一块创龙OMAPL138的板子,我要在上面成功移植Qt环境和触摸屏幕,这是我第二次进行Linux的Qt环境移植,发现了很多问题,需要重新整理。 我编译了,Qt5版本以上的,结果就是不成功,总是死在PDA问题上,在 configure文件上加入-xcb的选项,就算我安装了xcb所有的库文件,最后还是出问题,我还在研究之中,等着编译Qt5通过之后,我会重新写一个Linux编译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
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 文件夹中
对触摸屏信号的获取、校正、滤波处理,均采用开源的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
这里面需要注意的地方就是:
然后运行
./autoconfig.sh
执行结束后,我们查看一下是否安装成功,执行命令:
ls /home/delvis/opt/tslib1.4
如果出现bin,etc,include,lib这4个目录,如下图所示,说明交叉编译并安装tslib成功。 这个先放在这里了。
然后开始我们的重头戏,交叉编译Qt
交叉编译Qt成功之后,编译后的文件需要应用两个位置,一个部分需要移植到我们的开发板上,另一个部分需要把编译成功的库配置到我们运行QtCreator的PC机上,两个部分只有统一了才不会出问题。
将下载的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
创建架构信息:
cd ./qtbase/mkspecs/qws
mkdir omapl-g++
cd omapl-g++; touch qmake.config 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)
回到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文件。
还有一个比较重点的坑,就是我们上面提到的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*
都行。
执行命令启动编译:(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基本上就成功了。
这个部分不在赘述,我找到http://blog.csdn.net/u012175418/article/details/52704734 引用这个博客的“设置QT的交叉编译环境”,部分,按照这个步骤在PC机的Qt上面配置好我们刚编译的组件和刚刚编译Qt的交叉编译环境。
就是把我们刚才编译的环境拷贝到我们的目标板的目录下,可以使用FTP,可以使用OTG挂在U盘的方式,我喜欢使用SSH协议的scp命令。
sudo scp -r /home/delvis/work/qt-arm-4.8.6 [email protected]:/opt
输入密码之后就可以把整个文件夹传输到开发板上面了。
我们传输完毕之后,则需要在开发板上设定环境,当我们运行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
完成配置。
我们利用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· 尊重成果,本文将用的参考文献全部给出,向无私的工程师,爱好者致敬。
手里的创龙的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的平台)
sudo apt-get install u-boot-tools
命令安装)OMAPL138的Linux系统和引导都在SD里面,所以SD卡的分区十分重要,在这里要区分SD的分区和SDka分区的类型和名称。OMAPL138的文档中给出要分为两个区一个boot(2G大小,格式:b型 FAT32)和一个rootfs(不少于4G,85型 Linux文件系统)。
插上我们的USB读卡器,等待系统读取了SD卡的信息。输入:
sudo fdisk -l
得到下面截图的图片,这里有两个注意点,**fdisk命令十分危险,一定要看清楚后执行,博主曾经的时候因为输错一个字母导致整个机械硬盘格式化,花费了将近一天恢复。**重点在看清楚/dev/sdc
这个sdc,(插入不同的USB口Linux分配的挂在节点名称就不一样,可能是sdd,可能是sde,总之看清楚确认好是你的目标内存卡)
然后看倒数两行的信息,现在这个SD卡有两个分区,分别是sdc1和sdc2,我们现在要把这两个分区全部清除掉(你的可能有三四个分区,也可能只有一个分区)
上面的操作已经获得了/dev/sdc的设备节点名称,然后我们对/devsdc进行分区,需要建立两个分区,一个boot(FAT32型,大约2G大小)和rootfs区(Linux文件系统型,内存卡剩余大小全给它)
解挂SD卡:
sudo umount /dev/sdc*
这里加入通配符*就是解挂所有分区。
sudo fdisk /dev/sdc
会有一个这样的和用户交互的界面,可以输入p,回车,查看当前/dev/sdc分区状态。
然后我们删除所有分区,按照图片输入:
到此为止我们删除了所有的分区,然后我们就开始建立分区。
建立分区:
建立第二个分区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引导区的建立,在本文的文件附录下载压缩包mksdboot.rar文件,然后解压到你linux你任意的位置,你喜欢哪里就哪里,我解压到~/script/mksdboot
目录下了,以下所有命令都以这个路径为例,自己注意修改。
文件里面写入是这些内容
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目录里面的所有文件都拷贝到内存卡的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
如图,表示成功。(这里请使用绝对完全的路径,不要使用终端里面例如 ~ 这个符号代表的主目录)
这个就没有什么了,**把内存卡拔了再插一下,**在压缩包的filesystem里面有rootfs.tar.zb2文件,使用解压命令,解压到内存卡的rootfs区域即可。
sudo tar -xvf ~/script/mksdboot/filesystem/rootfs.tar.bz2 -C /media/delvis/rootfs
等待解压完成之后,一个启动盘就做好了。
通过运行这个脚本来进行:注意修改脚本里面的路径,在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
可以把内存卡拿下来,放在板子里启动了。注意调节拨码开关让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· 尊重成果,本文将用的参考文献全部给出,向无私的工程师,爱好者致敬。
IPC(Inter-Process Communication,进程间通讯)包含三种通信方式,信号量、共享内存和消息队列。在linux编程里面可以有两个不同的标准,一个是SYSTEM-V标准,一个是POSIX标准。以下是两个标准之间的区别1。简单的说,POSIX更轻量,常面向于线程;SYSTEM-V更重一些,需要深陷Linux内核之中,面向于进程。
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在进程层级和线程层级既有相似的点也有不同的点。
在Linux-用户空间-多线程与同步4中,引用了sem_xxx()的接口,根据上面的信息我们也可以知道这是POSIX IPC的接口。我们在那个文章中并没有阐述信号量和spinlock的区别,在网上大多数人只谈论到信号量和锁之间的用法上的区别,或者是意义上的区别。我这里想更进一步的解释信号量的实现和锁是有差别的。还有pthread_xxx里面的spinlock和内核的spinlock理念是一样的,但是调度完全不一样,pthread_xxx的可以关注这个实验4。
System V中提供的信号量接口更通用相比于POSIX标准的信号量9。我们从后面的API介绍上面可以看出,system V中的提供的信号量接口是非常复杂和繁琐的,虽然接口少,但是标识符,标志位特别多,而且里面定义了很多自己的结构体的结构,甚至出现了变长参数。根据文献9,我们大分部使用的都是二进制的信号量,但我相信,这些接口都不是白给的,肯定有更多的使用场景,可能需要以后工作情景上面进行挖掘了。
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。
创建一个新的信号量或取得一个已有的信号量的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失败。
改变信号量的值。
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.
根据文献9提供的示例,完成使用System V级别的信号量,创建简单的二进制信号量,满足以下条件:
关于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
在UNIX操作系统下运行的应用程序、服务器以及其他程序都被称为进程。每个进程都在CPU的虚拟内存上分配地址空间。每个进程的地址空间都是独立的,因此进程和进程不会意识对方的存在,觉得自己是CPU上唯一运行的进程。从处理器的角度,系统上有几个CPU,就最多能运行几个进程。但Linux是一个多任务的系统,内核为了支持多任务需要在不同的进程之间切换,这样从时间维度的划分造成了多进程同时运行的假象。
内核借助CPU的帮助,负责进程切换,欺骗进程独占了CPU。这就需要在切换进程之前保存进程的所有状态及相关要素,并将进程置于IDLE状态。在进程切换回来之前,这些资源和信息需要恢复到CPU和内存上面。这些资源被成为进程上下文。
内核除了要保存进程一些必要信息之外。还需要合理分配每一个进程的时间片,通常重要的进程得到CPU的时间多一点,次要的进程时间少一点。这个时间分配的过程成为调度。
本文对于进程的表述的结构如下所示:
这部分介绍一些从用户空间视角来观摩进程的概念。
Linux对进程的表述采用了一种层次结构,每个进程都依赖于一个父进程。内核启动init
程序作为第一个进程,该进程负责进一步的系统初始化操作,并显示视提示符和登陆界面。因此init
进程被称为进程树的根,所有的进程都直接或者间接起源自该进程。如下使用pstree程序的输出所示。
这个树形结构的进程上面和新进程的创建方式密切相关。UNIX操作系统有两种创建新进程的机制分别是fork()
和exec()
。
fork()
可以创建当前进程的一个副本且有独立的PID号码(父进程和子进程只有PID号码不同)。Linux使用了一个中所周的的技术来使fork
更高效,那就是写时复制(copy on write)操作。
exec()
将一个新程序加载到当前的内存中执行,旧程序的内存页面被逐出,其内容被替换为新数据,然后开始执行新的程序。
除了重量级进程(有时候也成为UNIX进程),还有一个轻量级进程(线程)。本质上,一个进程包含若干个线程,这些线程使用着同样的地址空间,共享同样的数据和资源,因此除了使用同步的方法保护共享的资源之外,没有额外的通信机制了。这也是线程和进程的差别。
Linux使用clone()
的方法创建线程。其工作方式类似于fork
,但启用了精准的检查,以确认那些资源与父进程共享、哪些资源为线程独立创建。
linux提供一些关于进程的系统调用,例如fork()
、getpid()
、getppid()
、wait()
、waitpid()
,可以创建进程,对父进程和子进程流程的一些控制。
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了两个进程:
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
可以观测到子进程是否已经退出,以及时回收进程资源。
进程优先级粗暴分为实时进程和非实时进程:
进程优先级模型转换为时间片长度,优先级越高的占用的时间片越多。这种方案被称为抢占式多任务处理(preemptive multitasking),即各个进程都被分配到一定的时间片用来执行。时间到了之后,内核会从当前进程收回控制权,让不同的进程运行。抢占的时候所有的CPU寄存器的内容和页表都会保存起来,因此其结果并不会因为进程的切换而丢失。
一个进程是有状态机可以表示的,我们可以分为:**运行(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系统调用,通知内核子进程已死。处于僵尸进程状态进程:
Linux有一套自己对于进程管理的方式,还有调度器,调度器可以理解为真正去执行和指挥进程如何运行的东西,而管理方式可以说是调度器去管理进程的一个笔记计划表(task_struct)在include/sched.h中,这里有如何管理进程,进程命名,编号法等等。
-Linux 通常把process當做是task,PCB (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
};
成员非常多,弄清楚费劲,可以分类:
<sched.h>
stcuct task_struct {
volatile long state;
};
<sched.h>
<resource.h>
struct rlimit {
unsigned long rlim_cur;
unsigned long rlim_max;
}
struct task_struct {
struct rlimite limit;
};
用户进程通过setrlimit()系统调用来增减当前限制,最大值不能超过rlim_max;getrlimits()用于检查当前限制。那么设定的资源是什么呢?
cat /proc/self/limits
来查看当前系统设定进程的资源限制。
Linux系统启动的时候会设定好当前资源限制的属性,在include/asm-generic/resource.h
中定义进程的资源限制,在linux启动的时候通过init进程完成配置。
在init_task.h中挂载该数组。
进程是由(二进制代码应用程序)、(单线程)、分配给应用程序的资源(内存、文件)。新进程是使用(fork)或者(exec)系统调用产生的。
命名空间是解决进程之间权限访问问题的一种设计机制。Linux的全局管理特性,比如PID、UID和系统调用uname返回系统的信息都是全局调用。因此就导致资源和重用的问题,在虚拟化中亟需解决。把全局资源通过命名空间抽象出来,划分不同的命名空间对应不同的资源分配,可实现虚拟化环境。
父命名空间生成多个子命名空间,子命名空间有映射关系到父命名空间。
#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,而且必须逐一指定需要支持的命名空间。如果内核编译的时候没有指定命名空间的支持,默认的命名空间的作用则类似于不启用命名空间,所有的属性相当于全局的。
<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, \
}
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的实例。
用户命名空间在数据结构管理方面类似于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;
}
我们耳熟能详的PID(process id)在其命名空间中唯一的标识号码,我们也可以通过PID找到一个进程,对进程进行操作。在进程的领域,并不是只有PID,也有其他很多的ID类型。这些概念延伸至进程组2。
Linux对于PID是要进行管理的,而管理手段对PID进行数据结构的表述,这些数据结构一只脚踏入task_struct中,还有一只脚在命名空间映射,而且对于繁琐的PID的数据结构查找,Linux内核也提供了若干辅助函数用于扫描PID的数据结构。
PID的分配,首要保证的是PID在命名空间上的唯一性,内核提供了这样的方法,alloc_pidmap
和free_pidmap
这样的方法可以分配和释放PID。
(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提供setpgid
,getpgrp
,getpgid
的系统调用。
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
除了pid
和tgid
还需要其他成员来管理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
};
//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)
除了ID链接之外,内核还负责管理建立在UNIX进程创建模型之上的“家族关系”。术语如下:
// <sched.h>
struct task_struct {
...
struct list_head children; /* list of my children */
struct list_head sibling; /* linkage in my parent’s children list */
...
}
task_struct
之间的互相联系可以表现如图所示3:
在这部分,我们来讨论fork
和exec
的系统调用实现。这部分其实在# 13_ARMv8_内存管理(一)-内存管理要素 有提及,但是是从内存管理的角度来看(内存分页技术在fork上面提供了写时复制功能,为其效率提升做了很大的贡献)。在这一节我们来了解一下fork
如何实现的。
同时,我们把fork
的系统调用实现,归纳到# 0x21_LinuxKernel_内核活动(一)之系统调用 的“可用系统调用”章节中去。
在Linux系统中,不仅仅只有一个fork
系统调用,还有其他系统调用,例如vfork
,clone
。fork
进程依赖于写时复制技术的实现。fork
、vfork
、clone
调用的内核函数分别是sys_fork
,sys_vfork
,sys_clone
函数。三个函数最终也都调用了do_fork
函数,这个函数内部大多数工作都是由copy_process
复制进程的内核函数完成的。
进程内部至少包含一个线程,因此进程复制中比较重要的处理过程就是对于线程的复制。每个线程都有自己独立的栈空间,因此这部分要很谨慎小心的处理。
进程复制涉及方方面面,比如对于进程一些共享信息的处理(内存),shmat
这样的系统调用就强调了在进程复制之前,复制后的行为4。
关于进程的系统调用分为:
fork()
:属于重量级调用,因为其建立了一个完整的进程副本,然后作为子进程去执行。为了减少工作量,linux内核使用了写时复制的的技术。由于写时复制技术的实现,vfork速度方面不再有优势,应避免使用vfork。vfork()
:类似于fork()
,但是不会创建一个进程副本,父进程和子进程之间共享一份数据。好处就是节约了大量的CPU,而坏处是,父进程或者子进程之间任意一个成员修改数据都能影响到对方。vfork
的设计用于子进程形成之后立即执行execve
系统调用5加载新程序的情形。在子进程退出或者加载新程序之前,内核保证父进程处于阻塞状态。clone()
:产生线程,可以对父子进程之间的共享、复制进行精准控制。写时复制(Copy-on-write, COW)技术,防止了父进程被复制之后建立真的数据,避免了使用大量的内存,复制时操作耗费更多的时间。如果复制之后执行exec立即加载程序,那么负面效应更加严重,甚至复制都是多余的,因为exec会把新数据的内存替换到当前进程的内存位置,然后运行。
写时复制技术在底层实现的是在fork的时候,只复制父进程的页表,而不是复制真正的数据,而且父子进程不允许修改彼此的物理页(共享内存的物理页除外), 这种实现很简单,通过标记页表为只读属性,无论父子进程在尝试访问内存的时候,由于访问了只读属性的页面,处理器会向内核报告访问段错误,接着在段错误的处理函数中开始真正的复制数据。
COW机制使得内核可能尽可能的延迟内存的复制,更重要的是,实际上在很多情况下不需要复制,这样节约了大量的时间。
do_fork:https://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
是架构级定义(在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, ®s, 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作为系统调用的结果返回,否则返回错误码。
早期内核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, ®s, 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
。
早期内核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, ®s, 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。
下图是一个进程的生命周期和相应的探针点:
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
信号。
do_fork
无论是最新版还是老的linux版本,都是内核级的实现,这部分给架构级的sys_fork函数调用。从这里可以看出,这部分已经不是和平台相关的代码了,纯属内核内部的软件逻辑。kernel/fork.c
do_fork
以调用copy_process
开始,后者执行新进程的实际工作(收尾工作)。我们暂时不去理会copy_process
内部做了什么。pid_nr_ns
;如果没有创建命名空间只在局部PID获取即可task_pid_vnr
。wake_up_new_task
唤醒。copy_process
是do_fork
中核心函数,任务是完成父进程的复制功能,这里面必须包含三个系统调用的请求处理fork\vfork\clone
。
这个复制过程比较复杂,我们大部分过程略过,具体解析参考《深入Linux内核架构》P55-P61,原版参考P66-P75。
父进程的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。
在linux系统中, 我们接触最多的莫过于用户空间的任务,像用户线程或用户进程,因为他们太活跃了,也太耀眼了以至于我们感受不到内核线程的存在,但是内核线程却在背后默默地付出着,如内存回收,脏页回写,处理大量的软中断等,如果没有内核线程那么linux世界是那么的可怕!7在进入我们真正的主题之前,我们需要知道一下事实:
我们知道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
可以输出方括号的进程为内核线程所属进程,与普通进程区分。
Linux提供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;
}
bprm_init
处理几个管理型的任务,包括mm_struct
初始化,mm_init
用于栈创建;prepare_binprm
:用于提供一些父进程相关的值UID和GID;search_binary_handler
用于do_execve
结束之后查找一个适当的二进制格式,用于执行特定的文件。
start_code
和end_code
指定该段在地址空间驻留区域。start_data
和end_data
之间。start_brk
和brk
指定边界。start_stack
定义。几乎所有的寄存机栈都是自动向下增长的。唯一的例外是PA-RISC。对于栈反向增长,体系结构相关部分的实现必须告知内核,可以通过设定STACK_GROWSUP
完成。arg_start
和arg_end
之间,还有env_end
和env_start
、除了ELF格式,还有几种Linux支持的二进制格式,这里列举作为参考:
在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
像内核注册。该函数的目的是向一个链表增加一种新的二进制格式。
进程必须exit
系统调用终止。这使得内核有机会将该进程的资源释放回系统。该调用入口点是sys_exit
函数,需要一个错误码作为其参数。很快退出进程调度委托给do_exit
。简言之,该函数的实现就是将各个引用计数器-1。
学习进入到了下一个阶段,还是以AD9833为例,这次学习是向设备申请中断,实现触发,在未来很多场景,比如做用户级的SPI传输完毕数据之后,怎么知道从设备要发数据呢,则需要一个IO信号通知主设备来读从设备的数据,那么就需要一个外部的IO中断信号,所以呢,对于中断的处理十分重要,本demo实现这样的一个功能**增加一个GPIO口,这个GPIO口接的是一个按键,通过触发信号,进入中断服务函数,在中断服务函数内改变AD9833的波形。**以此达到学习实验目的。
之前的代码都是一样的,在这里尽量剥离AD9833驱动和Linux的代码模板,只写中断相关相关程序。
效果演示视频: https://v.youku.com/v_show/id_XMzY4MjAwOTA0MA==.html?spm=a2h3j.8428770.3416059.1
原理图:
首先需要两个头文件:
#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 );
}
使用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" );
到此我们就完成了中断配置。
使用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日
通用的ELF文件,我们可以分为四大类:
----------------------------------------
ELF Header(描述基本属性)
----------------------------------------
.text (段1)
----------------------------------------
.data (段2)
----------------------------------------
.bss (段3)
----------------------------------------
...other
sections...
----------------------------------------
Section header tables (段表)
----------------------------------------
String Tables (字符串表)
Symbol Tables (符号表)
----------------------------------------
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)
段表的作用是,在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;
在其他段里面引用了一些段的地址,这些地址单独存成一个重定位表,例如printf("hello world"), 里面的字符串就要被重定位到一个单独的区域。
段名、变量名长度不固定,单独放在一个连续的区域里面,使用偏移来索引字符串。.strtab
,.shstrtab
字符串表, 在header里面 e_shstrndx: 段表字符串表所在的段在段表中的下标 表示
// 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;
}
Github地址:carloscn/uncle-ben-os at car_lab_06 (github.com)
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 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, 0x80000LDR x7, 0x800000
; 这条指令的意义是,将当前PC寄存器的地址的 + 0x80000的偏移,取出地址内容填充到x7寄存器中。从一个寄存器的值吐到内存中,支持立即数和寄存器操作。把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 16-bit的立即数到寄存器。
MOV xd, #<imm>
16位立即数
MOVZ xd, #<imm16>, LSL #<shift>
16位的立即数,逻辑左移动 16,32,48位
相比于LDR和STR指令(8 bytes),LDP和STP指令用于多字节(16 bytes)操作,
【释义】:
LDP x3, x7, [x0]
-> 从x0的值为基地址,加载地址到X3寄存器,存储x0+8到x7寄存器。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和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位
参考03_ARMv7-M_存储系统结构的地址对齐访问设计。我们对ARMv8架构的对齐操作进行整理。
和Cortex-M一样,独占和顺序(ordered)访问(相对于指令预取和乱序访问)不可以对非对齐的地址进行访问。但是所有的load和store是支持非对齐访问的。
LDM
, STM
, PUSH,
和 POP
指令不存在与A64指令集。所有的块传输都是通过STP
和LDP
LDNP
and STNP
instructions provide a streaming or non-temporal hint, that the data does not need to be retained in caches. 指令提供了一个流式或非临时提示,即数据不需要保留在缓存中。PRFM
, or prefetch memory instructions enable targeting of a prefetch to a specific cache level .“PRFM”或预取存储器指令能够将预取定为特定的高速缓存级别。char
, short
, int
and long long
in the same way when loading and storing quantities from memory. 例如,这使得在从内存加载和存储量时更容易以相同的方式处理“char”、“short”、“int”和“long-long”。当执行在AArch64模式的时候,需要对取指令的load和store操作需要使用栈指针,所以会对PC和SP进行对齐检查。
我一个研究嵌入式的,不知道怎么就迷上了上位机,接了几个项目都是关于Qt,这个项目还是比较经典的,自己没事儿的时候也进行研究,对这个软件进行升级,反正,我喜欢编程,喜欢研究这些东西。研究了一下午,查了很多资料,看了很多的例子,我对于JS是0基础,能稍微看懂一点点HTML语言的东西,下午调试了好几遍,运行了好几遍,终于在我更改了JavaScript的函数完毕之后,一下子数据回传成功了,那种感觉很棒!!!特地写这个博客,记录我的研发成果。
【正文】
QWebChannel官方给的例子是和Socket混合,看的是一头雾水,不得不吐槽Qt了,能不能单独讲讲使用方法,别和别的东西结合?!
对于我的GPS地图项目,研究又有了新的进展,我在本年度一直在业余时间研究Qt开发的软件和网页交互(JS和HTML),虽然我对于网页的知识仅仅有个基础,在业余方面,我也不断的在对该软件进行升级研究,目前状态:
到目前为止,GPS定位系统交互驱动百度地图已经完全适配QWebEngine组件。
红色方框部分为当前鼠标的经纬度信息,这个信息来源于html访问百度地图的JavaScript,然后回传给C++的信息。
本文重点来说一下,如何从JavaScript获取回传信息,实现交互
我是非专业的,我也没有找关于HTML和JS交互的书,在我研究的过程中我认为是一个这样的关系:
QWebEngine提供了调用HTML里面JavaScript的方法,这里HTML像是一个接口,在HTML尾部有一个这样的标签,<script> 函数和变量体 </script> ,在这个标签内的函数和变量体中写入一些函数和变量,这些函数和变量要么是JavaScript中的调用,要么是Qt中的调用,所以,HTML像是一个QT和网页的桥梁。在下面的章节,我们详细讨论一下步骤。
要使用QWebChannel要打点好两个方面,第一,Qt方面,需要包含QWebChannel类,注册好QWebChannel需要连接的Qt的对象;第二,JS方面,官方提供了配套的qwebchannel.js文件,这个js文件就相当于驻JavaScript负责通信的。所需准备的:
下面就这两方面讨论如何使用:
地图这块仅有一个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>
这么多代码,看的你很焦躁,不用太细研究,里面只是定义了很多百度地图读取的方法,我们把重点放在几个点上(你可以在上面的源码注释中找到重点标记)
<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!!!
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!!!
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!!!
map.addEventListener("mousemove",function(e) {
updateInfo(e.point.lng,e.point.lat);
});
当发生mousemove这个行为的时候,这可能是JS里面的知识,则调用function(e),一定要注意这个function不能直接把函数体写在这里,必须采用我们上面的方法,把函数体单独写,然后在内部写上调用。
到此javascript部分已经交代清楚了。
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,然后你该怎么办就怎么办了。
经过一下午的努力,完成了数据回传,可以将js的数据传递回来,但是QWebChannel这块还有其他的东西,比如和槽和信号连接,如果今后能遇到这个项目,我会继续研究,否则,到今天这个项目完毕。
感谢观看。
【木有】
1. 本文为MULTIBEANS团队研发跟随文章,未经允许不得转载。
2· 文中涉及的内容若有侵权行为,请与本人联系,本人会及时删除。
3· 尊重成果,本文将用的参考文献全部给出,向无私的工程师,爱好者致敬。
Linux基本配置完毕了,这几天开始着手Linux驱动的开发,从一个最简单的键盘驱动开始,逐步的了解开发驱动的过程有哪些。看了一下Linux3.3内核文件下的driver目录,点开里面的C文件,感觉底层的Linux驱动机制还是很复杂的,还需要一段漫长时间的学习。现在开发的也不能说是叫做驱动,也只能说是驱动的应用,我们学习驱动也从应用逐步开始,往里面深入吧。
我们主要使用USER0和USER1 KEY,两个按键,完成Linux GPIO键盘驱动开发。从图中可以看出GPIO0_6和GPIO6_1主要采集键盘按下的信息。
驱动结构可以看上图,主要是在注册Linux内核设备,我们使用platform_device进行内核注册。我们从思维到图上看,从后往前的顺序进行一个一个定义。
**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
就是我们写应用程序的时候,按键读出来的值,就可以判断了。
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
完成取值。
思维导图在向前,我们就需要定义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函数(可以为空)。
最后就是初始化函数和退出函数,最终要的就是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()里面写入的函数。
我们可以把这个驱动程序静态编译到内核代码树中也可以以模块的形式加载到内核中,我建议一开始开发的时候使用模块的形式加入到内核中,等着编译成熟之后,在编译到内核里面。
制定内核源码文件的路径,写好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
这里比较重要的:
编译完成之后,通过ls命令,查看生成了key.ko文件。
通过scp或者ftp把key.ko文件传输到目标板子上。
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命令查看内核输出日志。
建立文件: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目录,然后运行即可。
通过按键就可以输出这些值,意味着我们的驱动开发成功了。
链接: https://pan.baidu.com/s/1pNcEBEj 密码: xpsq
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文件)
本文使用Window环境为:
系统: Windows 10 64bits
QtCreator版本: Qt5.12.1 / MinGW 64版本
解压qwt-6.1.4文件会发现该文件是一个基于Qt的工程文件,使用本地的QtCreator自帶的qt 命令行工具,分别编译release版本。
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.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#
本文使用的Linux環境爲:
系統: Manjaro KDE 64 bit版本gcc version 8.2.1 20181127 (GCC)
Qt版本: Qt5.12.1
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.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的組件支持。
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#
IPC(Inter-Process Communication,进程间通讯)包含三种通信方式,信号量、共享内存和消息队列。在linux编程里面可以有两个不同的标准,一个是SYSTEM-V标准,一个是POSIX标准。以下是两个标准之间的区别1。简单的说,POSIX更轻量,常面向于线程;SYSTEM-V更重一些,需要深陷Linux内核之中,面向于进程。
在大数据量的交换领域,使用memcpy是一个十分占据CPU和内存带宽的方法,共享内存作为Linux进程之间的通信是一个比较高效和节约资源的方法。现代多核心处理器,还有一个处理方法,比如对于视频的处理VPU,设定专门VDMA的RTL来处理。
内存共享是随内核持续的,在两个毫无关系的进程中在userspace区域创建一个一块共享的内存,使两个进程都有读写的权限。既然是共享,必然涉及资源的竞争问题,故还要引入随内核持续的进程同步机制,来辅助访问共享的内存空间。如图所示,为进程的LMA在共享同一块share memory。
既然是内存,这里就不得不提到,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.
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.
因此,shmat会绕开BSS展开的空间,紧凑地在HEAP上分配空间。
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:
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:
具体参考文献3
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失败。
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
Stackoverflow: understand-shmat-and-attachment-to-the-process-memory ↩ ↩2
Linux Programmer's Manual - shmat, shmdt - System V shared memory operations ↩
Linux Programmer's Manual - shmget - allocates a System V shared memory segment ↩ ↩2
Linux Programmer's Manual - shmctl - System V shared memory control ↩ ↩2
在上一篇博客里面,基于OMAPL138的字符驱动_GPIO驱动AD9833(一)之ioctl 中使用#include <linux/miscdevice.h>
中的miscdevice机制,在呢篇博客中使用宋宝华的Linux驱动设备中提供的cdev机制完成注册,
根据参考文献[1]中所说:
misc设备其实也是字符设备,主不过misc设备驱动在字符设备的基础上又进行了一次封装,使用户可以更方便的使用。
在本次实验中确实印证了使用cdev比较复杂,且加载ko模块驱动之后还需要查看设备号,手动mknod节点,而且在卸载驱动的时候也是非常繁琐的,但在这里本着学习的目的也进行了实验,后续的开发会使用miscdevice机制而不使用cdev机制
本次实验主要针对字符设备的:
在上一篇博客基于OMAPL138的字符驱动_GPIO驱动AD9833(一)之ioctl,只能用ioctl函数进行一个字节的幻数进行指令通信,但无法传输类似于设置频率指令。如果传递这样的参数,只需要使用write和read函数完成数据的传递。
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
这个地方就会用到了。
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(..)那个。
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;
}
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设备的使命就完成了。
需要在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事件。
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把数据传递给用户,比较安全。
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日
Q1:为什么要使用动态链接?
Q2:动态链接有什么弊端?
Q3:Linux和windows操作系统动态链接有什么区别?
这里举个例子如何生成动态链接库
// 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 .
使用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 &
使用cat /proc/581540/maps
查看进程的的相关的内存映射,这里面主要涉及了3个so文件:
再来看下libsum.so的装载属性readelf -l libsum.so
Note,动态链接模块的装载地址是从0x0开始的,而真正的地址是0x7fd1e0cb1000,共享对象的最终装载地址在编译的时候不是确定的。
操作系统需要根据内存空闲情况,动态分配一块物理内存给程序,所以程序被装载的地址是不确定的。系统在装载程序的时候需要对程序的指令和数据中绝对地址的引用进行重定位,基地址变换之后加上偏移量就能拿到重定位后的地址。静态链接完成的是
链接时重定位,现在这种情况被叫做装载时重定位。
gcc -shared a.c -o a.so
此时使用装载时重定位,我理解这也是作为so文件共享对象必然要有的属性,动态链接库必然根据每个依赖他的程序不同,装载时进行重定位。
还记得我们在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
模块间的数据访问依托的是.GOT表(全局偏移表Global Offset Table),当代码引用外部的变量的时候,需要通过这个表来查找到偏移量。通过objdump -h exa.so
b的位置在0x10be0的位置,.got在0x10bd8,在其偏移量0x8的位置。
PIC的代码不包含任何代码段重定位表。
在调用外部库的时候<lib_sum@plt>
有个符号plt,这个就是延迟绑定技术。这个目的是,可以提高动态链接库的执行速度。在ELF文件中,比如有很多分支里面调用的函数,很低的概率被用到,那么这个函数会被延迟绑定,就是这个函数第一次被用到的时候才会被绑定(查找、重定位)。
我们在ELF段结构里面看到GOT表,就分为两种:
我们在查看linux进程的/proc/xxx/maps的时候,里面会有ld.so,这个是由系统提供的一个动态链接器(Dynamic Linker)。启动一个包含动态链接的程序的步骤:
在.interp
段指定ld.so的路径。通常Linux系统下,都是/lib/ld-linux.so.2这个文件。
在.dynamic
段保存了动态链接器的基本信息,地址、哈希表地址、等等。可以用readelf -d xx.so命令查看。ldd命令还可以查看elf依赖了哪些库。
作为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,这里需要补充的是,在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通信的内部原理和接口使用。
Piping:
$ echo "hello world" | grep hello
: echo的输出 -> stdout -> stdin -> grep$ echo “hello world” |& cat
: echo输出 -> stdout + stderr -> stdin -> grep$ anything |& cat
stderr -> stdin -> catRedirecting:
$ 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
carlos
[email protected]
$ 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)再加上文件索引,传输吞吐的效率并不会太高。
#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().
使用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;
}
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;
}
我觉得这个话题还是蛮重要的,我也查了不少的资料,网上有很多人做了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创建的子进程的标准输出里面拿到数据,就一定要等子进程执行完毕之后才可以拿到。我也做了相关的实验,得到的结论两种情况:
我这里设计一个例子,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()函数
如果去掉了fread()函数,则会在调用pclose之前,等待子进程的调用。
因此,可以将上述过程绘制出来, 调用fread的控制流如图所示:
没有调用fread的控制流如图所示:
我们这里对popen进程创建进行对比,并不是说popen是一个创建进程的方法,popen的确有创建进程的功能,但是我们应该把关注点放在建立管道通信上面。
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:
此时,可以看到一个错误,在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;
}
从实验结果看,pclose已经拿到了一个错误的返回值。
此时,可以看到一个错误,在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;
}
从实验结果看,pclose还是拿到了一个错误的返回值。
因此,建议popen不要和其他进程同步使用,否则会拿到一个错误的状态。除了状态的错误,运行的时序调度都是正确的。
此外,如果我们没有屏蔽掉SIG_CHLD信号,此时没有调用pclose(),那么就->可能<-会出现僵尸进程。
上层的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 **: 管道建立在同一个进程上面
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,子进程写,父进程读。
这里要注意的是:
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进程读。
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;
}
posted @ 2017-07-17 13:15 Carlos·Wei
很遗憾,QtWebkits在Qt5.6以上版本被淘汰了,对于这个接口良且和其他类例如QWebFrame完美结合的组件就这么没了,我只能表示惋惜。对于QtWebEngine新的组件,不得不承认它从Chromium继承过来的强大的性能,但接口上还不是很丰富,和其他类的交互也不是很完美,期待Qt能够对其进行进一步开发,我也会不断的升级Qt,尝试新的接口。
目前而言,QWebEngine有以下缺点:
基于我们的GPS定位项目,参考:[Qt开发北斗定位系统融合百度地图API及Qt程序打包发布] ,我们在该项目中使用的是Qt5.5版本,在嵌入的浏览器作为加载地图用的是QWebKits组件,我们将其升级使用QWebEngine进行加载地图,和HTML和JS进行交互。我们以此为例,进行简要的介绍。
你刚刚升级到Qt5.6版本可能在UI设计界面时候在组件中找不到QWebEngineView这个组件,无法从这里拖拽这个组件到你的UI界面上。我查阅了很多资料,看到别人经常使用 ui->webEngineview->...
这样,我甚至怀疑是否因为安装了其他版本的Qt影响到了我,我卸载了包含5.6版本的所有Qt,又重新安装了一遍,但是再重启软件后,依然没有发现QWebEngineView这个鬼东西。在Qt5.5中你也能发现有这样的组件QWebView,如图1所示:
QWebView组件可以通过QWebFrame来进行HTML和JS的通信,如果过渡到QWebEngineView,要是没有这个UI组件的话,我如何把浏览器嵌入到软件界面,实现网页和软件的混合编程呢。根据官方提供的一个例子中,cookiebrowser中找到了答案,这也是官方给的例子中,唯一一个嵌入到网页中的!(不得不说,Qt给的例子很模糊很差!) 经过研究, QWebEngineView使用widget组件,拖拉出来是一个透明的组件,对着组件按右键->promote to.. ->选择QWebEngineView,如图2,完成操作。
有了QWebEngineView这个UI组件,我们可以在程序中调用其成员、方法和函数完成操作了。
在使用方法上有很大的区别,可以说是两个完全不同理念的东西,这里为了更通俗易懂,就不粘贴API文档中函数解释,就用最常用的!
#include <QtWebEngineWidgets> // 基本组件
#include <QWebEnginePage> // HTML页面
#include <QWebChannel> // C++和JS/HTML双向通信,代替了已淘汰的QtWebFrame的功能
在我们的项目中一开始就要引入这样的组件,但在我们的项目中,没有频繁用到与JS的互相交互,所以这里暂时没有关于QWebChannel的使用方法,只留下这个接口。
以下为区别:
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》,可以解释这个槽函数的重要性。
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文件了。
到此,我们完成了两者的初始化。
我们以按钮的槽函数为例,当点击按钮时,会向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交互的关键。
还是以该槽函数为例:
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),报错。
通过这样的方法,我们就完成了一个初级的过渡。本人由于是研究嵌入式的程序员,只是上位机学习简单的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 **
计算最高为1的比特位前面有多少个0。例如, 0x0800 0000 0000 000F,前面是有4个0的(使用64位的寄存器),如果使用Wn寄存器,按照32位算。
CLZ <Xd>, <Xn>
clz x0, x1
[计算x1寄存器内的值最高位1的比特位前面有多少个0,并放入x0]比较指令有CMP和CMN,CMP本质为:CMP x1, x2
-> x1 = x1 - x2
;CMN为负向比较:CMN x1, x2
->x1 = x1 + x2
。
NZC = a - b,根据不同的比较结果,来确定NZC的标志位,若x1 > x2, NCZV = 0100,若x1 = x2, NCZV = 0110,若x1 < x2, NCZV = 1000。
CMP <Xd|SP>, #<imm>{, lsl <#shift>}
, note shift supports #0 and #12 only.cmp x1, #8
(x1 = x2 - 8)cmp x1, #8, lsl #12
( x1 = x2 - (8 << 12) )CMP <Xd>, <Xm>{, <shift> #<amount>}
, note #amount range 0 to 63cmp x1, x2, asr #2
NZC = a + b,a和b如果加和为负数,N置位; a + b的和为0,Z置位;a + b溢出(两个负数相加),C置位。
CMN <Xd|SP>, <R><m>, {<extend> {#<amount>}}
cmn x0, x1
( x0 = x0 - x1 )cmn x0, x1, lsl #5
( x0 = x0 - (x1 << 5) )CMN <Xd|SP>, #<imm>{, lsl <#shift>}
, note shift supports #0 and #12 only.cmn x1, #8
(x1 = x2 - 8)cmn x1, #8, lsl #12
( x1 = x2 - (8 << 12) )CMN <Xd>, <Xm>{, <shift> #<amount>}
, note #amount range 0 to 63cmn 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 |
(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
条件选择指令包括CSEL\CSET\CSINC三条指令,也是非常重要的指令。
CSEL, 条件选择指令,如果cond为真,那么xd就是xn的值,否则就是xm的值。注意,为上面表格的值。
CSEL <Xd>, <Xn>, <Xm>, <cond>
csel x1, x1, x2, EQ
-> 如果Z=1,x1维持原始值,否则x2给x1赋值csel x1, x1, x2, MI
-> 如果N=1,x1维持原始值,否则x2给x1赋值CSET, 条件置位指令,如果cond为真,那么xd就是1,否则就是0。注意,为上面表格的值。
CSET <Xd>, <cond>
cset x1, EQ
-> 如果Z=1,x1是1, 否则为0cset x1, MI
-> 如果N=1,x1是1, 否则为0CSINC,条件选择并增加指令 如果cond为真,那么xd就是xn的值,否则就是xm+1的值。注意,为上面表格的值。
CSINC <Xd>, <Xn>, <Xm>, <cond>
csinc x1, x1, x2, EQ
-> 如果Z=1,x1维持x1, 否则为x2+1csinc x1, x2, x3, MI
-> 如果N=1,x1为x2, 否则为x3+1用汇编实现下面的c代码:
unsigned long cel_test(unsigned long a, unsigned long b)
{
if (a == 0) {
return b + 2;
} else {
return b - 1;
}
}
分析拆解:
test_csel:
mov x2, #0x2
mov x3, #-0x1
cmp x0, #0
csel x4, x2, x3, EQ
add x0, x1, x4
ret
b.<cond>
,xx为以上表格的指,不返回Example:
分析:
调用顺序应该是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å
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.