0%

源码保护技术

对于一些大软件工程,可能有一部分模块代码是从第三方获取来的,并且可能是不开源的。前段时间给别的部门软件组提供了一份快充协议栈的模块,使用静态库的方式提供,提供头文件给用户调用协议栈中的函数及读取全局变量,快充协议栈的输入也是有模块内部调用用户提供的指定函数,或指定的全局变量实现,在工程最后链接的时候会将静态库中未链接的符号链接到外部代码的地址。

今天在看PAN1020的SDK,一款蓝牙SOC,它里面的协议栈是不开源的,厂家提供hex文件,于是在想用户需如何集成hex到自己工程,用户代码要如何与协议栈不开源的代码建立联系。自己的猜想基本是对的,厂家还会提供一份头文件,里面有用户需要调用的函数或全局变量,和静态库不同的是,头文件中不是变量和函数的声明,而是需要让用户知道,所需的全局变量与函数在这份hex文件中的哪个位置。

代码分析

厂家提供的hex文件需要链接到指定地址,以下是PAN1020未开源模块所提供的头文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#define STACK_FUN_ADDR					0x00016600

//app fun register
#define SVC_dbg_sys_write_register (*(volatile uint32_t *)(STACK_FUN_ADDR))
typedef void (*dbg_sys_write_register_handler)(void (*call)(uint16_t address, uint16_t data));

#define SVC_appm_init_register (*(volatile uint32_t *)(STACK_FUN_ADDR + 4))
typedef void (*appm_init_register_handler)(void (*call)(void));

#define SVC_hci_init_register (*(volatile uint32_t *)(STACK_FUN_ADDR + 8))
typedef void (*hci_init_register_handler)(void (*call)(bool reset));

#define SVC_hci_send_2_controller_register (*(volatile uint32_t *)(STACK_FUN_ADDR + 12))
typedef void (*hci_send_2_controller_register_handler)(void (*call)(void *param));

#define SVC_attm_svc_create_db_register (*(volatile uint32_t *)(STACK_FUN_ADDR + 16))
typedef void (*attm_svc_create_db_register_handler)(uint8_t (*call)(uint16_t *shdl, uint16_t uuid, uint8_t *cfg_flag, uint8_t max_nb_att,
uint8_t *att_tbl, ke_task_id_t const dest_id,const struct attm_desc *att_db, uint8_t svc_perm));

#define SVC_gattc_con_enable_register (*(volatile uint32_t *)(STACK_FUN_ADDR + 20))
typedef void (*gattc_con_enable_register_handler)(void (*call)(uint8_t conidx));

一开始,我以为是要将hex链接到0x00016600地址,头文件中给出的是函数在hex中的偏移地址。hex文件是带地址的,我用jflash一看后才明白,hex的起始地址是0,0x00016600地址上存放的是一个网表,用来存放函数的真实地址,网表其实就是函数指针类型的数组。

代码中的(STACK_FUN_ADDR + offset)其实就是函数指针的地址,所以(*(volatile uint32_t *)(STACK_FUN_ADDR + offset))就是取出函数指针的值,这个是函数真实所在的地址。头文件中还有函数指针的typedef,可以通过函数指针类型知道每个函数的形参和返回值。

有了函数地址与函数类型,用户就可以通过以下方式去调用库中的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void app_fun_resgister(void)
{
((appm_init_register_handler)SVC_appm_init_register)(appm_init);
((hci_init_register_handler)SVC_hci_init_register)(hci_init);
((hci_send_2_controller_register_handler)SVC_hci_send_2_controller_register)(hci_send_2_controller);
((attm_svc_create_db_register_handler)SVC_attm_svc_create_db_register)(attm_svc_create_db);
((gattc_con_enable_register_handler)SVC_gattc_con_enable_register)(gattc_con_enable);
((gattm_cleanup_register_handler)SVC_gattm_cleanup_register)(gattm_cleanup);
((gattm_create_register_handler)SVC_gattm_create_register)(gattm_create);
((prf_cleanup_register_handler)SVC_prf_cleanup_register)(prf_cleanup);
((prf_create_register_handler)SVC_prf_create_register)(prf_create);
((prf_get_id_from_task_register_handler)SVC_prf_get_id_from_task_register)(prf_get_id_from_task);
((prf_get_task_from_id_register_handler)SVC_prf_get_task_from_id_register)(prf_get_task_from_id);
((prf_init_register_handler)SVC_prf_init_register)(prf_init);
((attm_att_update_perm_register_handler)SVC_attm_att_update_perm_register)(attm_att_update_perm);
((gattm_init_attr_register_handler)SVC_gattm_init_attr_register)(gattm_init_attr);
((hci_basic_cmd_send_2_controller_register_handler)SVC_hci_basic_cmd_send_2_controller_register)(hci_basic_cmd_send_2_controller);
((prf_add_profile_register_handler)SVC_prf_add_profile_register)(prf_add_profile);
((gattc_get_mtu_register_handler)SVC_gattc_get_mtu_register)(gattc_get_mtu);
((attm_init_register_handler)SVC_attm_init_register)(attm_init);
((gattm_init_register_handler)SVC_gattm_init_register)(gattm_init);
((hci_send_2_host_register_handler)SVC_hci_send_2_host_register)(hci_send_2_host);
((attmdb_destroy_register_handler)SVC_attmdb_destroy_register)(attmdb_destroy);
((sleep_handler_register)SVC_sleep_handler_register)(sleep_handler);
((ble_event_handler_register)SVC_ble_event_handler_register)(ble_event_handler);
((rf_init_handler_register)SVC_rf_init_handler_register)(rf_init_handler);
((ble_rx_handler_register)SVC_ble_rx_handler_register)(ble_rx_handler);
((rf_dev_cal_init_handler_register)SVC_rf_dev_cal_init_handler_register)(rf_dev_cal_init_handler);
}

以上代码为初始化模块中,用户将模块所需要的一些函数注册到模块中。

思考

PAN1020这种方式保护源码,让用户不可见源码,相比我之前封静态库的方式有以下优势:

  1. 编译模块的时候不需要其他模块的头文件声明,通过模块对外提供注册函数的方式,获取外部函数地址后调用。我之前静态库的方式就需要在编译模块的时候提供其他模块的函数声明头文件,而且如果链接的时候找不到还会链接失败。
  2. 这种由外部注册函数的方式,模块所依赖其他模块的函数名可以关心,如果用静态库方式就外部函数的函数名就必须与声明的一致。
  3. 提供给用户hex,比静态库文件的信息更少,静态库中有网表信息,hex中只有二进制文件和地址。

是否可以将一个工程的各模块分开独立,编译成各个hex后再集成?优点:可以使公司的源码不易泄露,每个人都维护自己的模块。

让各个模块编译完hex后的地址都为0开始,hex中网表的前两个为模块init和模块main。用户将所有的hex链接到指定flash地址,配置给os各模块网表起始地址,网表大小,模块调度周期。因为网表的前两个是init和main,所以os可以轻松创建好任务。模块与模块之间调用,由模块向os请求获取特定模块特定网表index的函数地址。

Chinese translated version of Documentation/process/coding-style.rst

If you have any comment or update to the content, please post to LKML directly. However, if you have problem communicating in English you can also ask the Chinese maintainer for help. Contact the Chinese maintainer, if this translation is outdated or there is problem with translation.

Chinese maintainer: Zhang Le <r0bertz@gentoo.org>


Documentation/process/coding-style.rst 的中文翻译

如果想评论或更新本文的内容,请直接发信到LKML。如果你使用英文交流有困难的话, 也可以向中文版维护者求助。如果本翻译更新不及时或者翻译存在问题,请联系中文版 维护者:

1
2
3
4
5
6
7
中文版维护者: 张乐 Zhang Le <r0bertz@gentoo.org>
中文版翻译者: 张乐 Zhang Le <r0bertz@gentoo.org>
中文版校译者: 王聪 Wang Cong <xiyou.wangcong@gmail.com>
wheelz <kernel.zeng@gmail.com>
管旭东 Xudong Guan <xudong.guan@gmail.com>
Li Zefan <lizf@cn.fujitsu.com>
Wang Chen <wangchen@cn.fujitsu.com>

以下为正文


Linux 内核代码风格

这是一个简短的文档,描述了 linux 内核的首选代码风格。代码风格是因人而异的, 而且我不愿意把自己的观点强加给任何人,但这就像我去做任何事情都必须遵循的原则 那样,我也希望在绝大多数事上保持这种的态度。请 (在写代码时) 至少考虑一下这里 的代码风格。

首先,我建议你打印一份 GNU 代码规范,然后不要读。烧了它,这是一个具有重大象征性意义的动作。

不管怎样,现在我们开始:

1) 缩进

制表符是 8 个字符,所以缩进也是 8 个字符。有些异端运动试图将缩进变为 4 (甚至 2!) 字符深,这几乎相当于尝试将圆周率的值定义为 3。

理由:缩进的全部意义就在于清楚的定义一个控制块起止于何处。尤其是当你盯着你的 屏幕连续看了 20 小时之后,你将会发现大一点的缩进会使你更容易分辨缩进。

现在,有些人会抱怨 8 个字符的缩进会使代码向右边移动的太远,在 80 个字符的终端屏幕上就很难读这样的代码。这个问题的答案是,如果你需要 3 级以上的缩进,不管用 何种方式你的代码已经有问题了,应该修正你的程序。

简而言之,8 个字符的缩进可以让代码更容易阅读,还有一个好处是当你的函数嵌套太深的时候可以给你警告。留心这个警告

在 switch 语句中消除多级缩进的首选的方式是让 switch 和从属于它的 case 标签对齐于同一列,而不要 两次缩进 case 标签。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
switch (suffix) {
case 'G':
case 'g':
mem <<= 30;
break;
case 'M':
case 'm':
mem <<= 20;
break;
case 'K':
case 'k':
mem <<= 10;
/* fall through */
default:
break;
}

不要把多个语句放在一行里,除非你有什么东西要隐藏:

1
2
if (condition) do_this;
do_something_everytime;

也不要在一行里放多个赋值语句。内核代码风格超级简单。就是避免可能导致别人误读 的表达式。

除了注释、文档和 Kconfig 之外,不要使用空格来缩进,前面的例子是例外,是有意为 之。

选用一个好的编辑器,不要在行尾留空格。

2) 把长的行和字符串打散

代码风格的意义就在于使用平常使用的工具来维持代码的可读性和可维护性。

每一行的长度的限制是 80 列,我们强烈建议您遵守这个惯例。

长于 80 列的语句要打散成有意义的片段。除非超过 80 列能显著增加可读性,并且不 会隐藏信息。子片段要明显短于母片段,并明显靠右。这同样适用于有着很长参数列表 的函数头。然而,绝对不要打散对用户可见的字符串,例如 printk 信息,因为这样就 很难对它们 grep。

3) 大括号和空格的放置

C 语言风格中另外一个常见问题是大括号的放置。和缩进大小不同,选择或弃用某种放置策略并没有多少技术上的原因,不过首选的方式,就像 Kernighan 和 Ritchie 展示 给我们的,是把起始大括号放在行尾,而把结束大括号放在行首,所以:

1
2
3
if (x is true) {
we do y
}

这适用于所有的非函数语句块 (if, switch, for, while, do)。比如:

1
2
3
4
5
6
7
8
9
10
switch (action) {
case KOBJ_ADD:
return "add";
case KOBJ_REMOVE:
return "remove";
case KOBJ_CHANGE:
return "change";
default:
return NULL;
}

不过,有一个例外,那就是函数:函数的起始大括号放置于下一行的开头,所以:

1
2
3
4
int function(int x)
{
body of function
}

全世界的异端可能会抱怨这个不一致性是… 呃… 不一致的,不过所有思维健全的人 都知道 (a) K&R 是 正确的 并且 (b) K&R 是正确的。此外,不管怎样函数都是特 殊的 (C 函数是不能嵌套的)。

注意结束大括号独自占据一行,除非它后面跟着同一个语句的剩余部分,也就是 do 语 句中的 “while” 或者 if 语句中的 “else”,像这样:

1
2
3
do {
body of do-loop
} while (condition);

1
2
3
4
5
6
7
if (x == y) {
..
} else if (x > y) {
...
} else {
....
}

理由:K&R。

也请注意这种大括号的放置方式也能使空 (或者差不多空的) 行的数量最小化,同时不失可读性。因此,由于你的屏幕上的新行是不可再生资源 (想想 25 行的终端屏幕),你 将会有更多的空行来放置注释。

当只有一个单独的语句的时候,不用加不必要的大括号。

1
2
if (condition)
action();

1
2
3
4
if (condition)
do_this();
else
do_that();

这并不适用于只有一个条件分支是单语句的情况;这时所有分支都要使用大括号:

1
2
3
4
5
6
if (condition) {
do_this();
do_that();
} else {
otherwise();
}

3.1) 空格

Linux 内核的空格使用方式 (主要) 取决于它是用于函数还是关键字。(大多数) 关键字后要加一个空格。值得注意的例外是 sizeof, typeof, alignof 和 __attribute__,这 些关键字某些程度上看起来更像函数 (它们在 Linux 里也常常伴随小括号而使用,尽管 在 C 里这样的小括号不是必需的,就像 struct fileinfo info; 声明过后的 sizeof info)。

所以在这些关键字之后放一个空格:

1
if, switch, case, for, do, while

但是不要在 sizeof, typeof, alignof 或者 attribute 这些关键字之后放空格。 例如,

1
s = sizeof(struct file);

不要在小括号里的表达式两侧加空格。这是一个 反例

1
s = sizeof( struct file );

当声明指针类型或者返回指针类型的函数时, * 的首选使用方式是使之靠近变量名或者函数名,而不是靠近类型名。例子:

1
2
3
char *linux_banner;
unsigned long long memparse(char *ptr, char **retptr);
char *match_strdup(substring_t *s);

在大多数二元和三元操作符两侧使用一个空格,例如下面所有这些操作符:

1
=  +  -  <  >  *  /  %  |  &  ^  <=  >=  ==  !=  ?  :

但是一元操作符后不要加空格:

1
&  *  +  -  ~  !  sizeof  typeof  alignof  __attribute__  defined

后缀自加和自减一元操作符前不加空格:

1
++  --

前缀自加和自减一元操作符后不加空格:

1
++  --

.-> 结构体成员操作符前后不加空格。

不要在行尾留空白。有些可以自动缩进的编辑器会在新行的行首加入适量的空白,然后 你就可以直接在那一行输入代码。不过假如你最后没有在那一行输入代码,有些编辑器 就不会移除已经加入的空白,就像你故意留下一个只有空白的行。包含行尾空白的行就 这样产生了。

当 git 发现补丁包含了行尾空白的时候会警告你,并且可以应你的要求去掉行尾空白; 不过如果你是正在打一系列补丁,这样做会导致后面的补丁失败,因为你改变了补丁的上下文。

4) 命名

C 是一个简朴的语言,你的命名也应该这样。和 Modula-2 和 Pascal 程序员不同, C 程序员不使用类似 ThisVariableIsATemporaryCounter 这样华丽的名字。C 程序员会 称那个变量为 tmp ,这样写起来会更容易,而且至少不会令其难于理解。

不过,虽然混用大小写的名字是不提倡使用的,但是全局变量还是需要一个具描述性的名字。称一个全局函数为 foo 是一个难以饶恕的错误。

全局变量 (只有当你 真正 需要它们的时候再用它) 需要有一个具描述性的名字,就像全局函数。如果你有一个可以计算活动用户数量的函数,你应该叫它 count_active_users() 或者类似的名字,你不应该叫它 cntuser()

在函数名中包含函数类型 (所谓的匈牙利命名法) 是脑子出了问题——编译器知道那些类 型而且能够检查那些类型,这样做只能把程序员弄糊涂了。难怪微软总是制造出有问题的程序。

本地变量名应该简短,而且能够表达相关的含义。如果你有一些随机的整数型的循环计数器,它应该被称为 i 。叫它 loop_counter 并无益处,如果它没有被误解的可能的话。类似的, tmp 可以用来称呼任意类型的临时变量。

如果你怕混淆了你的本地变量名,你就遇到另一个问题了,叫做函数增长荷尔蒙失衡综合症。请看第六章 (函数)。

5) Typedef

不要使用类似 vps_t 之类的东西。

对结构体和指针使用 typedef 是一个 错误 。当你在代码里看到:

1
vps_t a;

这代表什么意思呢?

相反,如果是这样

1
struct virtual_container *a;

你就知道 a 是什么了。

很多人认为 typedef 能提高可读性 。实际不是这样的。它们只在下列情况下有用:

  1. 完全不透明的对象 (这种情况下要主动使用 typedef 来 隐藏 这个对象实际上是什么)。

    例如: pte_t 等不透明对象,你只能用合适的访问函数来访问它们。

    Note:不透明性和 “访问函数” 本身是不好的。我们使用 pte_t 等类型的原因在于真的是完全没有任何共用的可访问信息。

  2. 清楚的整数类型,如此,这层抽象就可以 帮助 消除到底是 int 还是 long 的混淆。

    u8/u16/u32 是完全没有问题的 typedef,不过它们更符合类别 (d) 而不是这里。

    Note:要这样做,必须事出有因。如果某个变量是 unsigned long ,那么没有必要 typedef unsigned long myflags_t;

    不过如果有一个明确的原因,比如它在某种情况下可能会是一个 unsigned int 而在其他情况下可能为 unsigned long ,那么就不要犹豫,请务必使用 typedef。

  3. 当你使用 sparse 按字面的创建一个 类型来做类型检查的时候。

  4. 和标准 C99 类型相同的类型,在某些例外的情况下。

    虽然让眼睛和脑筋来适应新的标准类型比如 uint32_t 不需要花很多时间,可是有些人仍然拒绝使用它们。

    因此,Linux 特有的等同于标准类型的 u8/u16/u32/u64 类型和它们的有符号类型是被允许的——尽管在你自己的新代码中,它们不是强制要求要使用的。

    当编辑已经使用了某个类型集的已有代码时,你应该遵循那些代码中已经做出的选择。

  5. 可以在用户空间安全使用的类型。

    在某些用户空间可见的结构体里,我们不能要求 C99 类型而且不能用上面提到的 u32 类型。因此,我们在与用户空间共享的所有结构体中使用 __u32 和类似 的类型。

可能还有其他的情况,不过基本的规则是 永远不要 使用 typedef,除非你可以明确的应用上述某个规则中的一个。

总的来说,如果一个指针或者一个结构体里的元素可以合理的被直接访问到,那么它们就不应该是一个 typedef。

6) 函数

函数应该简短而漂亮,并且只完成一件事情。函数应该可以一屏或者两屏显示完 (我们 都知道 ISO/ANSI 屏幕大小是 80x24),只做一件事情,而且把它做好。

一个函数的最大长度是和该函数的复杂度和缩进级数成反比的。所以,如果你有一个理论上很简单的只有一个很长 (但是简单) 的 case 语句的函数,而且你需要在每个 case 里做很多很小的事情,这样的函数尽管很长,但也是可以的。

不过,如果你有一个复杂的函数,而且你怀疑一个天分不是很高的高中一年级学生可能甚至搞不清楚这个函数的目的,你应该严格遵守前面提到的长度限制。使用辅助函数, 并为之取个具描述性的名字 (如果你觉得它们的性能很重要的话,可以让编译器内联它们,这样的效果往往会比你写一个复杂函数的效果要好。)

函数的另外一个衡量标准是本地变量的数量。此数量不应超过 5-10 个,否则你的函数 就有问题了。重新考虑一下你的函数,把它分拆成更小的函数。人的大脑一般可以轻松的同时跟踪 7 个不同的事物,如果再增多的话,就会糊涂了。即便你聪颖过人,你也可能会记不清你 2 个星期前做过的事情。

在源文件里,使用空行隔开不同的函数。如果该函数需要被导出,它的 EXPORT 宏 应该紧贴在它的结束大括号之下。比如:

1
2
3
4
5
int system_is_up(void)
{
return system_state == SYSTEM_RUNNING;
}
EXPORT_SYMBOL(system_is_up);

在函数原型中,包含函数名和它们的数据类型。虽然 C 语言里没有这样的要求,在 Linux 里这是提倡的做法,因为这样可以很简单的给读者提供更多的有价值的信息。

7) 集中的函数退出途径

虽然被某些人声称已经过时,但是 goto 语句的等价物还是经常被编译器所使用,具体形式是无条件跳转指令。

当一个函数从多个位置退出,并且需要做一些类似清理的常见操作时,goto 语句就很方便了。如果并不需要清理操作,那么直接 return 即可。

选择一个能够说明 goto 行为或它为何存在的标签名。如果 goto 要释放 buffer, 一个不错的名字可以是 out_free_buffer: 。别去使用像 err1:err2: 这样的GW_BASIC 名称,因为一旦你添加或删除了 (函数的) 退出路径,你就必须对它们重新编号,这样会难以去检验正确性。

使用 goto 的理由是:

  • 无条件语句容易理解和跟踪
  • 嵌套程度减小
  • 可以避免由于修改时忘记更新个别的退出点而导致错误
  • 让编译器省去删除冗余代码的工作 ;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int fun(int a)
{
int result = 0;
char *buffer;

buffer = kmalloc(SIZE, GFP_KERNEL);
if (!buffer)
return -ENOMEM;

if (condition1) {
while (loop1) {
...
}
result = 1;
goto out_free_buffer;
}
...
out_free_buffer:
kfree(buffer);
return result;
}

一个需要注意的常见错误是 一个 err 错误 ,就像这样:

1
2
3
4
err:
kfree(foo->bar);
kfree(foo);
return ret;

这段代码的错误是,在某些退出路径上 foo 是 NULL。通常情况下,通过把它分离 成两个错误标签 err_free_bar:err_free_foo: 来修复这个错误:

1
2
3
4
5
err_free_bar:
kfree(foo->bar);
err_free_foo:
kfree(foo);
return ret;

理想情况下,你应该模拟错误来测试所有退出路径。

8) 注释

注释是好的,不过有过度注释的危险。永远不要在注释里解释你的代码是如何运作的: 更好的做法是让别人一看你的代码就可以明白,解释写的很差的代码是浪费时间

一般的,你想要你的注释告诉别人你的代码做了什么,而不是怎么做的。也请你不要把注释放在一个函数体内部:如果函数复杂到你需要独立的注释其中的一部分,你很可能需要回到第六章看一看。你可以做一些小注释来注明或警告某些很聪明 (或者槽糕) 的做法,但不要加太多。你应该做的,是把注释放在函数的头部,告诉人们它做了什么, 也可以加上它做这些事情的原因。

当注释内核 API 函数时,请使用 kernel-doc 格式。请看 Documentation/doc-guide/ 和 scripts/kernel-doc 以获得详细信息。

长 (多行) 注释的首选风格是:

1
2
3
4
5
6
7
8
/*
* This is the preferred style for multi-line
* comments in the Linux kernel source code.
* Please use it consistently.
*
* Description: A column of asterisks on the left side,
* with beginning and ending almost-blank lines.
*/

对于在 net/ 和 drivers/net/ 的文件,首选的长 (多行) 注释风格有些不同。

1
2
3
4
5
6
/* The preferred comment style for files in net/ and drivers/net
* looks like this.
*
* It is nearly the same as the generally preferred comment style,
* but there is no initial almost-blank line.
*/

注释数据也是很重要的,不管是基本类型还是衍生类型。为了方便实现这一点,每一行 应只声明一个数据 (不要使用逗号来一次声明多个数据)。这样你就有空间来为每个数据 写一段小注释来解释它们的用途了。

9) 你已经把事情弄糟了

这没什么,我们都是这样。可能你的使用了很长时间 Unix 的朋友已经告诉你 GNU emacs 能自动帮你格式化 C 源代码,而且你也注意到了,确实是这样,不过它 所使用的默认值和我们想要的相去甚远 (实际上,甚至比随机打的还要差——无数个猴子 在 GNU emacs 里打字永远不会创造出一个好程序) (译注:Infinite Monkey Theorem)

所以你要么放弃 GNU emacs,要么改变它让它使用更合理的设定。要采用后一个方案, 你可以把下面这段粘贴到你的 .emacs 文件里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
(defun c-lineup-arglist-tabs-only (ignored)
"Line up argument lists by tabs, not spaces"
(let* ((anchor (c-langelem-pos c-syntactic-element))
(column (c-langelem-2nd-pos c-syntactic-element))
(offset (- (1+ column) anchor))
(steps (floor offset c-basic-offset)))
(* (max steps 1)
c-basic-offset)))

(add-hook 'c-mode-common-hook
(lambda ()
;; Add kernel style
(c-add-style
"linux-tabs-only"
'("linux" (c-offsets-alist
(arglist-cont-nonempty
c-lineup-gcc-asm-reg
c-lineup-arglist-tabs-only))))))

(add-hook 'c-mode-hook
(lambda ()
(let ((filename (buffer-file-name)))
;; Enable kernel mode for the appropriate files
(when (and filename
(string-match (expand-file-name "~/src/linux-trees")
filename))
(setq indent-tabs-mode t)
(setq show-trailing-whitespace t)
(c-set-style "linux-tabs-only")))))

这会让 emacs 在 ~/src/linux-trees 下的 C 源文件获得更好的内核代码风格。

不过就算你尝试让 emacs 正确的格式化代码失败了,也并不意味着你失去了一切:还可以用 indent

不过,GNU indent 也有和 GNU emacs 一样有问题的设定,所以你需要给它一些命令选项。不过,这还不算太糟糕,因为就算是 GNU indent 的作者也认同 K&R 的权威性 (GNU 的人并不是坏人,他们只是在这个问题上被严重的误导了),所以你只要给 indent 指定选项 -kr -i8 (代表 K&R,8 字符缩进),或使用 scripts/Lindent 这样就可以以最时髦的方式缩进源代码。

indent 有很多选项,特别是重新格式化注释的时候,你可能需要看一下它的手册。 不过记住: indent 不能修正坏的编程习惯。

10) Kconfig 配置文件

对于遍布源码树的所有 Kconfig* 配置文件来说,它们缩进方式有所不同。紧挨着 config 定义的行,用一个制表符缩进,然而 help 信息的缩进则额外增加 2 个空格。举个例子:

1
2
3
4
5
6
7
8
config AUDIT
bool "Auditing support"
depends on NET
help
Enable auditing infrastructure that can be used with another
kernel subsystem, such as SELinux (which requires this for
logging of avc messages output). Does not do system-call
auditing without CONFIG_AUDITSYSCALL.

而那些危险的功能 (比如某些文件系统的写支持) 应该在它们的提示字符串里显著的声 明这一点:

1
2
3
4
config ADFS_FS_RW
bool "ADFS write support (DANGEROUS)"
depends on ADFS_FS
...

要查看配置文件的完整文档,请看 Documentation/kbuild/kconfig-language.txt。

11) 数据结构

如果一个数据结构,在创建和销毁它的单线执行环境之外可见,那么它必须要有一个引用计数器。内核里没有垃圾收集 (并且内核之外的垃圾收集慢且效率低下),这意味着你绝对需要记录你对这种数据结构的使用情况。

引用计数意味着你能够避免上锁,并且允许多个用户并行访问这个数据结构——而不需要担心这个数据结构仅仅因为暂时不被使用就消失了,那些用户可能不过是沉睡了一阵或者做了一些其他事情而已。

注意上锁 不能 取代引用计数。上锁是为了保持数据结构的一致性,而引用计数是一个内存管理技巧。通常二者都需要,不要把两个搞混了。

很多数据结构实际上有 2 级引用计数,它们通常有不同 的用户。子类计数器统计子类用户的数量,每当子类计数器减至零时,全局计数器减一。

这种 多级引用计数 的例子可以在内存管理 (struct mm_struct: mm_users 和 mm_count),和文件系统 (struct super_block: s_count 和 s_active) 中找到。

记住:如果另一个执行线索可以找到你的数据结构,但这个数据结构没有引用计数器, 这里几乎肯定是一个 bug。

12) 宏,枚举和RTL

用于定义常量的宏的名字及枚举里的标签需要大写。

1
#define CONSTANT 0x12345

在定义几个相关的常量时,最好用枚举。

宏的名字请用大写字母,不过形如函数的宏的名字可以用小写字母。

一般的,如果能写成内联函数就不要写成像函数的宏。

含有多个语句的宏应该被包含在一个 do-while 代码块里:

1
2
3
4
5
#define macrofun(a, b, c)                       \
do { \
if (a == 5) \
do_this(b, c); \
} while (0)

使用宏的时候应避免的事情:

  1. 影响控制流程的宏:
1
2
3
4
5
#define FOO(x)                                  \
do { \
if (blah(x) < 0) \
return -EBUGGERED; \
} while (0)

非常 不好。它看起来像一个函数,不过却能导致 调用 它的函数退出;不要打 乱读者大脑里的语法分析器。

  1. 依赖于一个固定名字的本地变量的宏:
1
#define FOO(val) bar(index, val)

可能看起来像是个不错的东西,不过它非常容易把读代码的人搞糊涂,而且容易导致看起来不相关的改动带来错误。

  1. 作为左值的带参数的宏: FOO(x) = y;如果有人把 FOO 变成一个内联函数的话,这 种用法就会出错了。
  2. 忘记了优先级:使用表达式定义常量的宏必须将表达式置于一对小括号之内。带参数 的宏也要注意此类问题。
1
2
#define CONSTANT 0x4000
#define CONSTEXP (CONSTANT | 3)
  1. 在宏里定义类似函数的本地变量时命名冲突:
1
2
3
4
5
6
#define FOO(x)                          \
({ \
typeof(x) ret; \
ret = calc_ret(x); \
(ret); \
})

ret 是本地变量的通用名字 - __foo_ret 更不容易与一个已存在的变量冲突。

cpp 手册对宏的讲解很详细。gcc internals 手册也详细讲解了 RTL,内核里的汇编语言经常用到它。

13) 打印内核消息

内核开发者应该是受过良好教育的。请一定注意内核信息的拼写,以给人以好的印象。 不要用不规范的单词比如 dont,而要用 do not 或者 don't 。保证这些信息简单明了,无歧义。

内核信息不必以英文句号结束。

在小括号里打印数字 (%d) 没有任何价值,应该避免这样做。

<linux/device.h> 里有一些驱动模型诊断宏,你应该使用它们,以确保信息对应于正确的设备和驱动,并且被标记了正确的消息级别。这些宏有:dev_err(), dev_warn(), dev_info() 等等。对于那些不和某个特定设备相关连的信息,<linux/printk.h> 定义了 pr_notice(), pr_info(), pr_warn(), pr_err() 和其他。

写出好的调试信息可以是一个很大的挑战;一旦你写出后,这些信息在远程出错时能提供极大的帮助。然而打印调试信息的处理方式同打印非调试信息不同。其他 pr_XXX() 函数能无条件地打印,pr_debug() 却不;默认情况下它不会被编译,除非定义了 DEBUG 或设定了 CONFIG_DYNAMIC_DEBUG。实际这同样是为了 dev_dbg(),一个相关约定是在一个已经开启了 DEBUG 时,使用 VERBOSE_DEBUG 来添加 dev_vdbg()。

许多子系统拥有 Kconfig 调试选项来开启 -DDEBUG 在对应的 Makefile 里面;在其他情况下,特殊文件使用 #define DEBUG。当一条调试信息需要被无条件打印时,例如,如果已经包含一个调试相关的 #ifdef 条件,printk(KERN_DEBUG …) 就可被使用。

14) 分配内存

内核提供了下面的一般用途的内存分配函数: kmalloc(), kzalloc(), kmalloc_array(), kcalloc(), vmalloc() 和 vzalloc()。 请参考 API 文档以获取有关它们的详细信息。

传递结构体大小的首选形式是这样的:

1
p = kmalloc(sizeof(*p), ...);

另外一种传递方式中,sizeof 的操作数是结构体的名字,这样会降低可读性,并且可能会引入 bug。有可能指针变量类型被改变时,而对应的传递给内存分配函数的 sizeof 的结果不变。

强制转换一个 void 指针返回值是多余的。C 语言本身保证了从 void 指针到其他任何指针类型的转换是没有问题的。

分配一个数组的首选形式是这样的:

1
p = kmalloc_array(n, sizeof(...), ...);

分配一个零长数组的首选形式是这样的:

1
p = kcalloc(n, sizeof(...), ...);

两种形式检查分配大小 n * sizeof(…) 的溢出,如果溢出返回 NULL。

15) 内联弊病

有一个常见的误解是 内联 是 gcc 提供的可以让代码运行更快的一个选项。虽然使 用内联函数有时候是恰当的 (比如作为一种替代宏的方式,请看第十二章),不过很多情况下不是这样。inline 的过度使用会使内核变大,从而使整个系统运行速度变慢。 因为体积大内核会占用更多的指令高速缓存,而且会导致 pagecache 的可用内存减少。 想象一下,一次 pagecache 未命中就会导致一次磁盘寻址,将耗时 5 毫秒。5 毫秒的 时间内 CPU 能执行很多很多指令。

一个基本的原则是如果一个函数有 3 行以上,就不要把它变成内联函数。这个原则的一个例外是,如果你知道某个参数是一个编译时常量,而且因为这个常量你确定编译器在编译时能优化掉你的函数的大部分代码,那仍然可以给它加上 inline 关键字。 kmalloc() 内联函数就是一个很好的例子。

人们经常主张给 static 的而且只用了一次的函数加上 inline,如此不会有任何损失, 因为没有什么好权衡的。虽然从技术上说这是正确的,但是实际上这种情况下即使不加 inline gcc 也可以自动使其内联。而且其他用户可能会要求移除 inline,由此而来的争论会抵消 inline 自身的潜在价值,得不偿失。

16) 函数返回值及命名

函数可以返回多种不同类型的值,最常见的一种是表明函数执行成功或者失败的值。这样 的一个值可以表示为一个错误代码整数 (-Exxx=失败,0=成功) 或者一个 成功 布尔值 (0=失败,非0=成功)。

混合使用这两种表达方式是难于发现的 bug 的来源。如果 C 语言本身严格区分整形和布尔型变量,那么编译器就能够帮我们发现这些错误… 不过 C 语言不区分。为了避免 产生这种 bug,请遵循下面的惯例:

1
2
如果函数的名字是一个动作或者强制性的命令,那么这个函数应该返回错误代
码整数。如果是一个判断,那么函数应该返回一个 "成功" 布尔值。

比如, add work 是一个命令,所以 add_work() 在成功时返回 0,在失败时返回 -EBUSY。类似的,因为 PCI device present 是一个判断,所以 pci_dev_present() 在成功找到一个匹配的设备时应该返回 1,如果找不到时应该返回 0。

所有 EXPORTed 函数都必须遵守这个惯例,所有的公共函数也都应该如此。私有 (static) 函数不需要如此,但是我们也推荐这样做。

返回值是实际计算结果而不是计算是否成功的标志的函数不受此惯例的限制。一般的, 他们通过返回一些正常值范围之外的结果来表示出错。典型的例子是返回指针的函数, 他们使用 NULL 或者 ERR_PTR 机制来报告错误。

17) 不要重新发明内核宏

头文件 include/linux/kernel.h 包含了一些宏,你应该使用它们,而不要自己写一些它们的变种。比如,如果你需要计算一个数组的长度,使用这个宏

1
#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))

类似的,如果你要计算某结构体成员的大小,使用

1
#define FIELD_SIZEOF(t, f) (sizeof(((t*)0)->f))

还有可以做严格的类型检查的 min() 和 max() 宏,如果你需要可以使用它们。你可以自己看看那个头文件里还定义了什么你可以拿来用的东西,如果有定义的话,你就不应在你的代码里自己重新定义。

18) 编辑器模式行和其他需要罗嗦的事情

有一些编辑器可以解释嵌入在源文件里的由一些特殊标记标明的配置信息。比如,emacs 能够解释被标记成这样的行:

1
-*- mode: c -*-

或者这样的:

1
2
3
4
5
/*
Local Variables:
compile-command: "gcc -DMAGIC_DEBUG_FLAG foo.c"
End:
*/

Vim 能够解释这样的标记:

1
/* vim:set sw=8 noet */

不要在源代码中包含任何这样的内容。每个人都有他自己的编辑器配置,你的源文件不应该覆盖别人的配置。这包括有关缩进和模式配置的标记。人们可以使用他们自己定制的模式,或者使用其他可以产生正确的缩进的巧妙方法。

19) 内联汇编

在特定架构的代码中,你可能需要内联汇编与 CPU 和平台相关功能连接。需要这么做时就不要犹豫。然而,当 C 可以完成工作时,不要平白无故地使用内联汇编。在可能的情况下,你可以并且应该用 C 和硬件沟通。

请考虑去写捆绑通用位元 (wrap common bits) 的内联汇编的简单辅助函数,别去重复地写下只有细微差异内联汇编。记住内联汇编可以使用 C 参数。

大型,有一定复杂度的汇编函数应该放在 .S 文件内,用相应的 C 原型定义在 C 头文 件中。汇编函数的 C 原型应该使用 asmlinkage

你可能需要把汇编语句标记为 volatile,用来阻止 GCC 在没发现任何副作用后就把它 移除了。你不必总是这样做,尽管,这不必要的举动会限制优化。

在写一个包含多条指令的单个内联汇编语句时,把每条指令用引号分割而且各占一行, 除了最后一条指令外,在每个指令结尾加上 nt,让汇编输出时可以正确地缩进下一条 指令:

1
2
3
asm ("magic %reg1, #42\n\t"
"more_magic %reg2, %reg3"
: /* outputs */ : /* inputs */ : /* clobbers */);

20) 条件编译

只要可能,就不要在 .c 文件里面使用预处理条件 (#if, #ifdef);这样做让代码更难阅读并且更难去跟踪逻辑。替代方案是,在头文件中用预处理条件提供给那些 .c 文件使用,再给 #else 提供一个空桩 (no-op stub) 版本,然后在 .c 文件内无条件地调用 那些 (定义在头文件内的) 函数。这样做,编译器会避免为桩函数 (stub) 的调用生成任何代码,产生的结果是相同的,但逻辑将更加清晰。

最好倾向于编译整个函数,而不是函数的一部分或表达式的一部分。与其放一个 ifdef 在表达式内,不如分解出部分或全部表达式,放进一个单独的辅助函数,并应用预处理 条件到这个辅助函数内。

如果你有一个在特定配置中,可能变成未使用的函数或变量,编译器会警告它定义了但未使用,把它标记为 __maybe_unused 而不是将它包含在一个预处理条件中。(然而,如果一个函数或变量总是未使用,就直接删除它。)

在代码中,尽可能地使用 IS_ENABLED 宏来转化某个 Kconfig 标记为 C 的布尔 表达式,并在一般的 C 条件中使用它:

1
2
3
if (IS_ENABLED(CONFIG_SOMETHING)) {
...
}

编译器会做常量折叠,然后就像使用 #ifdef 那样去包含或排除代码块,所以这不会带来任何运行时开销。然而,这种方法依旧允许 C 编译器查看块内的代码,并检查它的正确性 (语法,类型,符号引用,等等)。因此,如果条件不满足,代码块内的引用符号就不存在时,你还是必须去用 #ifdef。

在任何有意义的 #if 或 #ifdef 块的末尾 (超过几行的),在 #endif 同一行的后面写下注解,注释这个条件表达式。例如:

1
2
3
#ifdef CONFIG_SOMETHING
...
#endif /* CONFIG_SOMETHING */

附录 I) 参考

The C Programming Language, 第二版 作者:Brian W. Kernighan 和 Denni M. Ritchie. Prentice Hall, Inc., 1988. ISBN 0-13-110362-8 (软皮), 0-13-110370-9 (硬皮).

The Practice of Programming 作者:Brian W. Kernighan 和 Rob Pike. Addison-Wesley, Inc., 1999. ISBN 0-201-61586-X.

GNU 手册 - 遵循 K&R 标准和此文本 - cpp, gcc, gcc internals and indent, 都可以从 http://www.gnu.org/manual/ 找到

WG14 是 C 语言的国际标准化工作组,URL: http://www.open-std.org/JTC1/SC22/WG14/

Kernel process/coding-style.rst,作者 greg@kroah.com 发表于 OLS 2002: http://www.kroah.com/linux/talks/ols_2002_kernel_codingstyle_talk/html/

如何从Git仓库中将模块分离成独立仓库并保留其提交历史?

起因

  • 部门代码管理使用的SVN,由于Git可以提交至本地,自己在本地又使用Git管理代码,使用Git管理了一整个工程,在开发不同模块时切到不同分支,一开始没把模块独立放到一个Git仓库,现在想独立模块,又想保留其提交历史,故有此文。

参考

操作

  1. 将子目录拆分独立库:git subtree split -P <name-of-folder> -b <name-of-new-branch>,注意此行命令需要在Git仓库toplevel目录执行,<name-of-folder>需要避免在前面加./,避免使用反斜杠,否则会产生assertion failed errors,解决方案就是改掉就行,参考:assertion failed errors when trying to git subtree split
  2. 拆分独立库后此仓库在<name-of-new-branch>分支中会保存模块代码和提交记录,然后mkdir <name-of-new-branch> && cd <name-of-new-branch> && git,我参考别人的操作使用git pull </path/to/big-repo> <name-of-new-branch>拉取上级目录的指定分支到一个新的文件夹好像不太行,网上暂时没查到/path/to的用法。我使用的方式是将老仓库<name-of-new-branch>分支代码push,然后使用git pull <repo-path.git> <name-of-new-branch>拉来的代码。
  3. 上一步操作后就已经将代码和历史提交记录全部拉到了一个新仓库,后续就可以将新仓库Push。

如何在工程的Git仓库中引用模块仓库?

  1. 参考Git文档:Git 工具 - 子模块

    我们首先将一个已存在的 Git 仓库添加为正在工作的仓库的子模块。 你可以通过在 git submodule add 命令后面加上想要跟踪的项目的相对或绝对 URL 来添加新的子模块。 在本例中,我们将会添加一个名为 “DbConnector” 的库。
    $ git submodule add https://github.com/chaconinc/DbConnector
    Cloning into ‘DbConnector’…
    remote: Counting objects: 11, done.
    remote: Compressing objects: 100% (10/10), done.
    remote: Total 11 (delta 0), reused 11 (delta 0)
    Unpacking objects: 100% (11/11), done.
    Checking connectivity… done.
    默认情况下,子模块会将子项目放到一个与仓库同名的目录中,本例中是 “DbConnector”。 如果你想要放到其他地方,那么可以在命令结尾添加一个不同的路径。

  2. 如果一个分支有子模块,另一个分支没有,直接 checkout 后会造成子模块的文件在另一个分支未被删除,可以在 checkout 前使用git submodule deinit --all命令来卸载当前分支已安装的所有子模块,在 checkout 后,如果另一个分支也有子模块,可以使用git submodule init命令注册子模块,再使用git submodule update从子模块库中取出文件。

  3. 我使用的子模块库是本地的Git仓库,在拉库的时候首次会报Transmission type 'file' not allowed的错误,需通过git config --global protocol.file.allow always配置Git,参考:Git clone –recurse-submodules throws error on MacOs: Transmission type ‘file’ not allowed

Git子模块和子树区别

Git子模块(submodule)和子树(subtree)都是Git中用于将外部仓库包含到自己的仓库中的机制。虽然它们的目的相似,但在功能和使用方式上有一些区别。

Git子模块:

  • 子模块是对外部仓库中特定提交的引用。
  • 当你将一个子模块添加到你的仓库时,你在自己的仓库中包含了指向另一个仓库的链接,它作为一个子目录存在。
  • 子模块维护着独立的Git历史,被视为独立的仓库。它们有自己的分支、标签和提交历史。
  • 每个子模块引用指向外部仓库中的特定提交。你可以通过显式地拉取变更来更新子模块到新的提交。
  • 子模块通常用于在你的仓库中包含另一个项目作为依赖项,但希望保持两个代码库的分离。

Git子树:

  • 子树允许你直接将外部仓库的内容嵌入到自己仓库的子目录中。
  • 当你向你的仓库添加子树时,你将另一个仓库的文件导入并合并到你的仓库的子目录中。导入的文件成为你的仓库历史的一部分。
  • 子树不维护独立的Git历史。相反,外部仓库的提交会合并到你的仓库的历史中。
  • 子树允许你在你的仓库中直接对导入的代码进行修改。如果你有写入权限,你也可以将修改的内容推送回原始仓库。
  • 子树通常用于将另一个项目的代码作为你仓库的一部分,并将其视为你代码库的一个组成部分。

总结而言,子模块提供了一种将外部仓库作为独立实体包含在你的仓库中的方式,而子树允许你将外部仓库的内容合并到你的仓库历史中。选择使用子模块还是子树取决于你的具体需求和工作流程。

字符串转换符

使用#运算符可以将宏参数替换为一个字符串,并用双引号括起来,例如:

1
2
3
4
5
6
#define PRINT_STR(x) printf("The string is: %s\n", #x)

int main() {
PRINT_STR(Hello World);
return 0;
}

在这个示例程序中,定义了一个宏PRINT_STR,它的参数x通过#运算符被转换为一个字符串,并被传递给printf函数进行输出。运行这个程序会输出:The string is: Hello World

字符串拼接

在C语言中,宏定义可以使用##运算符进行字符串拼接,称为连接运算符(Token Pasting Operator)。##运算符可以将两个标记(Token)连接成一个标记,从而实现字符串拼接的功能。例如:

1
2
3
4
5
6
7
#define CONCAT(x, y) x##y

int main() {
int xy = 10;
printf("%d\n", CONCAT(x, y));
return 0;
}

在这个示例程序中,定义了一个宏CONCAT,它使用##运算符将两个参数x和y连接成一个标记。在main函数中定义了一个变量xy,并将连接后的标记xy作为参数传递给printf函数进行输出。输出结果是:10

变长参数宏

C语言中的变长参数宏(Variadic Macro)可以接受可变数量的参数。变长参数宏是通过使用特殊的预处理符号__VA_ARGS__来实现的,它表示可变参数的列表。下面是一个简单的示例,展示了如何使用变长参数宏:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

#define PRINTF(...) printf(__VA_ARGS__)

int main() {
int x = 10;
char* str = "hello";
PRINTF("x = %d, str = %s\n", x, str);
return 0;
}

在这个示例程序中,定义了一个宏PRINTF,它使用printf函数打印可变数量的参数。使用__VA_ARGS__表示可变参数的列表。在main函数中,PRINTF宏被调用,传递了三个参数,包括一个整数和一个字符串。输出结果是:x = 10, str = hello

宏:取最大值

一种方式:#define MAX(a, b) ((a) > (b) ? (a) : (b))
这种方式需要注意参数如果有自增运算,需要在传参的时候加括号,否则自增运算会重复两次。
正确调用方法如:z = MAX((x++), (y++));

使用下面一种方式可以避免此类问题

1
2
3
4
5
6
#define MAX(a, b) ({ \
typeof(a) _a = (a); \
typeof(b) _b = (b); \
(void)(&_a == &_b); \
_a > _b ? _a : _b; \
})

typeof是GNU C编译器内置的一种类型描述符,用于表示表达式的类型。在编译时,编译器会根据typeof(x)对象的类型生成一个类型的值,并将其插入到代码中。

比较难理解的是(void)(&_a == &_b);,看起来很多余,仔细分析一下,你会发现这条语句很有意思。它的作用有两个:

  1. 用来给用户提示一个警告,对于不同类型的指针比较,编译器会发出一个警告, 提示两种数据的类型不同。warning: comparison of distinct pointer types lacks a cast.
  2. 两个数进行比较运算,运算的结果却没有用到,有些编译器可能会给出一个warning,加一个(void)后,就可以消除这个警告。

宏:offsetof

offsetof是一个宏,用于获取结构体中成员的偏移量。实现是使用指针运算来计算结构体成员的偏移量。具体实现:

1
#define offsetof(type, member) ((size_t) &((type *)0)->member)

其中,type表示结构体类型,member表示结构体成员。在宏展开时,(type *)0将一个空指针强制转换为结构体指针类型,然后通过指针运算获取成员的地址。由于空指针的地址为0,因此可以确保这个地址不会指向任何实际的内存位置,避免了访问非法内存的风险。最后,将成员的地址转换为size_t类型的偏移量,并返回。

需要注意的是,offsetof宏只能用于标准布局的结构体,即结构体中的成员按照其定义顺序依次存储,没有嵌套、位域、虚函数等。对于非标准布局的结构体,offsetof可能无法正确计算成员的偏移量。

宏:container_of

container_of是一个宏,用于从结构体的成员指针计算出结构体的地址。其实现通常基于offsetof宏和指针运算。实现如下:

1
2
#define container_of(ptr, type, member) \
((type *)((char *)(ptr) - offsetof(type, member)))

其中,ptr表示结构体成员的指针,type表示结构体类型,member表示结构体成员名。offsetof(type, member)用于计算结构体成员在结构体中的偏移量,(char *)(ptr)将成员指针转换为char类型指针,以便进行指针运算。通过成员指针的地址减去成员在结构体中的偏移量,可以得到结构体的地址。最后,将地址转换为type类型的指针,并返回。

container_of宏常用于实现Linux内核中的数据结构,如链表、哈希表等。它可以方便地从链表节点或哈希桶中获取对应的数据结构。

参考文档

复合字面量介绍

假设给带int类型的形参函数传递一个值,可以传递int类型的变量,也可以传递int类型常量,但是对于带数组形参的函数则不一样,可以传递数组,但是不支持传递数组常量,由此C99新增了复合字面量的用法,字面量是指除符号常量外的常量。

例如:10是int的类型的字面量,10.24是double类型的字面量,”abc”是字符串的字面量等,如果有数组或者结构体的字面量,这样使用起来会更方便。

数组字面量

数组的复合字面量和数组初始化列表差不多,前面使用括号括起来的类型名。
例如,这是个数组定义:int age[2] = [19, 20];
使用复合字面量创建一个匿名数组:(int [2]){19, 20};
可见去掉定义中的数组名,留下的int[2]就是复合字面量的类型名,整个就是数组字面量。
使用数组字面量时可以像定义数组一样省略数组大小,也可以应用于二维或多维数组。
还可构造一个字符串数组,将复合字面量强制转换为指向其第一个元素的指针,如:char **foo = (char *[]) { "x", "y", "z" };

结构体字面量

结构体的复合字面量的指定类型是一个结构体。
假设,struct foo 和 structure 声明如:struct foo {int a; char b[2];} structure;
使用复合字面量构造 struct foo 的示例:structure = ((struct foo) {x + y, 'a', 0});
这等效于:{ struct foo temp = {x + y, 'a', 0}; structure = temp; }

复合字面量生命周期

在 C 语言中,复合字面量指定具有静态或自动存储持续时间的未命名对象。在 C++ 中,复合字面量指定一个临时对象,该对象仅在其完整表达式结束之前存在。因此,采用复合字面量的子对象地址的定义良好的 C 代码在 C++ 中可以是未定义的,因此 G++ 拒绝将临时数组转换为指针。例如,如果上面的数组复合字面量示例出现在函数内部,则在 C++ 中对 foo 的任何后续使用都会产生未定义的行为,因为数组的生命周期在 foo 的声明之后结束。

作为一种优化,G++ 有时会为数组复合字面量提供更长的生命周期:当数组出现在函数外部或具有 const 限定类型时。如果 foo 及其初始值设定项具有 char *const 而不是 char * 类型的元素,或者如果 foo 是全局变量,则数组将具有静态存储持续时间。但是,避免在 C++ 代码中使用数组复合字面量可能是最安全的。

复合字面量应用

  1. 函数参数是结构体、数组,用复合字面量传参。
  2. 程序运行中想给char数组赋字符串。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct foo {int a; int b; int c; int d; int e;} g_stTest;

volatile struct foo testFun2(int a, int b, int c, int d, int e)
{
struct foo stTest;
stTest.a = a;
stTest.b = b;
stTest.c = c;
stTest.d = d;
stTest.e = e;
return stTest;
};

volatile void testFun1(void)
{
g_stTest = testFun2(1, 2, 3, 4, 5);
}

反汇编

用gcc工具链编译s32k144平台的代码,通过ozone分析elf得到反汇编如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
volatile void testFun1(void)
{
// 0002C74C PUSH {R4-R5, R7, LR}
// 0002C74E SUB SP, SP, #32
// 0002C750 ADD R7, SP, #8
g_stTest = testFun2(1, 2, 3, 4, 5);
// 0002C752 LDR R4, =g_stTest ; [PC, #40] [0x0002C77C] =0x20002318
// 0002C754 MOV R2, R7
// 0002C756 MOVS R3, #4
// 0002C758 STR R3, [SP, #0]
// 0002C75A MOVS R3, #5
// 0002C75C STR R3, [SP, #4]
// 0002C75E MOV R0, R2
// 0002C760 MOVS R1, #1
// 0002C762 MOVS R2, #2
// 0002C764 MOVS R3, #3
// 0002C766 BL testFun2 ; 0x0002C710
// 0002C76A MOV R5, R4
// 0002C76C MOV R4, R7
// 0002C76E LDM R4!, {R0-R3}
// 0002C770 STM R5!, {R0-R3}
// 0002C772 LDR R3, [R4]
// 0002C774 STR R3, [R5]
}
// 0002C776 ADDS R7, #24
// 0002C778 MOV SP, R7
// 0002C77A POP {R4-R5, R7, PC}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
volatile struct foo testFun2(int a, int b, int c, int d, int e)
{
// 0002C710 PUSH {R4-R5, R7}
// 0002C712 SUB SP, SP, #44
// 0002C714 ADD R7, SP, #0
// 0002C716 STR R0, [R7, #12]
// 0002C718 STR R1, [R7, #8]
// 0002C71A STR R2, [R7, #4]
// 0002C71C STR R3, [R7]
struct foo stTest;
stTest.a = a;
// 0002C71E LDR R3, [R7, #8]
// 0002C720 STR R3, [R7, #20]
stTest.b = b;
// 0002C722 LDR R3, [R7, #4]
// 0002C724 STR R3, [R7, #24]
stTest.c = c;
// 0002C726 LDR R3, [R7]
// 0002C728 STR R3, [R7, #28]
stTest.d = d;
// 0002C72A LDR R3, [R7, #56]
// 0002C72C STR R3, [R7, #32]
stTest.e = e;
// 0002C72E LDR R3, [R7, #60]
// 0002C730 STR R3, [R7, #36]
return stTest;
// 0002C732 LDR R3, [R7, #12]
// 0002C734 MOV R5, R3
// 0002C736 ADD.W R4, R7, #20
// 0002C73A LDM R4!, {R0-R3}
// 0002C73C STM R5!, {R0-R3}
// 0002C73E LDR R3, [R4]
// 0002C740 STR R3, [R5]
};
// 0002C742 LDR R0, [R7, #12]
// 0002C744 ADDS R7, #44
// 0002C746 MOV SP, R7
// 0002C748 POP {R4-R5, R7}
// 0002C74A BX LR

分析

  1. 在进入函数后,首先会将R4, R5, R7寄存器压栈,使用PUSH指令后SP会自动减去压栈空间的大小。
  2. 然后是SP减去一个直接数,是在给局部变量、形参和参数传递开辟栈区空间。
  3. R7 = SP + 偏移,R7是局部变量所在栈区的地址,偏移的大小是函数内调用函数时,参数传递所需要的空间大小。
  4. g_stTest = testFun2(1, 2, 3, 4, 5);反汇编中,参数4, 5通过栈传递,参数1, 2, 3通过R1~R3寄存器传递。
  5. testFun2函数进入后,STR R0, [R7, #12]等指令,会将R0~R3寄存器传递的参数存入(局部变量)栈中,为什么传参的还有R0?
  6. 重新分析g_stTest = testFun2(1, 2, 3, 4, 5);反汇编,R0传入的是R7(局部变量地址),在testFun2函数return stTest;时,将stTest写入R0传入的地址区域。
  7. 在退出函数时,如果函数有return参数,会将return参数写入R0,若大于4字节会将进入函数时R0传入的地址返回。
  8. 在退出函数时,会将SP改回进入函数前的SP,并使用POP指令出栈(POP指令会增加SP),最后跳转出函数(还可以直接POP取出PC跳转)。

网站:Learn Git Branching

分离HEAD

HEAD 是一个对当前检出记录的符号引用,也就是指向你正在其基础上进行工作的提交记录。HEAD 总是指向当前分支上最近一次提交记录。大多数修改提交树的 Git 命令都是从改变 HEAD 的指向开始的。HEAD 通常情况下是指向分支名的。在你提交时,改变了分支的状态,这一变化通过 HEAD 变得可见。
如果想看 HEAD 指向,可以通过 cat .git/HEAD 查看, 如果 HEAD 指向的是一个引用,还可以用 git symbolic-ref HEAD 查看它的指向。

相对引用

切换至上个版本git checkout HEAD^
切换至上上上版本git checkout HEAD~3

强制修改分支位置

我使用相对引用最多的就是移动分支。可以直接使用 -f 选项让分支指向另一个提交。例如:git branch -f main HEAD~3,命令会将 main 分支强制指向 HEAD 的第 3 级父提交。

撤销变更

git reset 通过把分支记录回退几个提交记录来实现撤销改动。你可以将这想象成“改写历史”。git reset 向上移动分支,原来指向的提交记录就跟从来没有提交过一样。
为了撤销更改并分享给别人,我们需要使用 git revert,在我们要撤销的提交记录后面居然多了一个新提交,此次提交是用来撤销上一次提交的,此次提交与上上一次提交状态相同。

Git Cherry-pick

如果你想将一些提交复制到当前所在的位置(HEAD)下面的话, Cherry-pick 是最直接的方式了。命令形式为:git cherry-pick <提交号>...

交互式的 Rebase

交互式 rebase 指的是使用带参数 –interactive 的 rebase 命令, 简写为 -i。
如果你在命令后增加了这个选项, Git 会打开一个 UI 界面并列出将要被复制到目标分支的备选提交记录,它还会显示每个提交记录的哈希值和提交说明,提交说明有助于你理解这个提交进行了哪些更改。Git会自动打开默认的文本编辑器,比如在我本地使用的是vim。在文本编辑器的开始几行会列出所有满足条件的提交记录,每个提交记录对应一行,每行开头单词代表要对这条提交记录实施的操作。

操作 说明
pick 采用该提交(默认行为)
reword 采用该提交,但要求修改提交记录的备注
edit 采用该提交,但要求修改提交记录的信息,如:作者名称,邮箱地址等
squash 采用该提交,但它会被并入前一条提交
fixup 类似“squash”,但是会丢弃这条提交记录的日志信息
exec 执行指定的shell脚本或命令
drop 丢弃该提交

Git Describe

git describe 的​​语法是:git describe <ref>
<ref> 可以是任何能被 Git 识别成提交记录的引用,如果你没有指定的话,Git 会以你目前所检出的位置(HEAD)。
它输出的结果:<tag>_<numCommits>_g<hash>
tag 表示的是离 ref 最近的标签, numCommits 是表示这个 ref 与 tag 相差有多少个提交记录, hash 表示的是你所给定的 ref 所表示的提交记录哈希值的前几位。当 ref 提交记录上有某个标签时,则只输出标签名称。

Git Pull

git pull 就是 git fetch 和 git merge 的缩写!

rebase和merge区别【阅读原文

使用git merge 命令将 master 分支合并到 feature分支中:git merge feature master,git merge 会在 feature 分支中新增一个新的 merge commit,然后将两个分支的历史联系在一起。

  • 使用 merge 是很好的方式,因为它是一种非破坏性的操作,对现有分支不会以任何方式被更改。
  • 另一方面,这也意味着 feature 分支每次需要合并上游更改时,它都将产生一个额外的合并提交。
  • 如果master 提交非常活跃,这可能会严重污染你的 feature 分支历史记录。不过这个问题可以使用高级选项 git log 来缓解。

使用git rebase 命令将 master 分支合并到 feature分支中:git rebase feature master

  • rebase 会将整个 feature 分支移动到 master 分支的顶端,从而有效地整合了所有 master 分支上的提交。
  • 但是,与 merge 提交方式不同,rebase 通过为原始分支中的每个提交创建全新的 commits 来重写项目历史记录,特点是仍然会在feature分支上形成线性提交。
  • rebase 的主要好处是可以获得更清晰的项目历史。首先,它消除了 git merge 所需的不必要的合并提交;其次,正如你在上图中所看到的,rebase 会产生完美线性的项目历史记录,你可以在 feature分支上没有任何分叉的情况下一直追寻到项目的初始提交。

Pull Request

如果你是在一个大的合作团队中工作, 很可能是main被锁定了, 需要一些Pull Request流程来合并修改。如果你直接提交(commit)到本地main, 然后试图推送(push)修改, 你将会收到这样类似的信息:
! [远程服务器拒绝] main -> main (TF402455: 不允许推送(push)这个分支; 你必须使用pull request来更新这个分支.)

起因

用Github部署静态站访问国内速度慢,主要表现有2个,一是首次请求页面打开慢,二是打开后图片加载慢。问题一原因是域名解析需要访问多个海外的DNS服务器,且Github静态站服务器也在海外,首次请求国内或本地没有缓存。问题二也是访问Github静态站服务器速度受限,图片和文本并发访问,文本比图片数据量小先加载完成显示。

本想将博客全部移到Gitee上,尝试部署又遇到三个问题,一是不能使用自己的域名解析,二是部署Gitee静态站服务它竟说我有文章不合规,三是每次上传Gitee后不会自动更新静态页面,每次要重新发布审核。

本人使用的解决办法是将图片上传到gitee,github静态博客上的图片都使用gitee的链接,Gitee/blog仓库img分支上传了本博客用到的图片。

部署方式

参考:Gitee Pages

在Gitee中创建仓库,开然后将博客用到的图片上传到Gitee中,开通仓库的Pages服务更新分支。审核成功后就可以通过Gitee提供的域名,加上图片在仓库中的路径访问了。例如我的静态页面网址是:https://xxx.gitee.io/,要访问仓库210430-at32文件夹中的210430-at32-1.jpg,访问网址为:https://xxx.gitee.io/210430-at32/210430-at32-1.jpg

使用域名转发

加入以后我的图床地址变了,但我又不想重新修改每个文章里的图片网址怎么办?

使用域名转发可以解决此问题,我使用的是易名注册的域名和易名免费的域名解析服务,开启URL隐性转发,转发值为你的静态页面网址,这样就可以通过自己的域名访问了,以后想换个图床的话只需要转发地址更改下就可以了。使用易名域名解析转发到我的静态页面会审核不通过,这是因为静态页面没有内容,只需要加个index.html骗骗审核就行了,审核结束就可以删除。

遇到一个问题,使用域名转发通过http访问可以,通过https访问不行,暂且文章里的图片网址用http访问吧。

前言

这篇文章是对之前文章【LTE Cat.1模块和阿里云物联网平台使用】的一个补充。之前只介绍了阿里云物联网平台如何创建产品、添加设备、添加物模型概述,缺少对消息解析、物模型展示的使用介绍,导致我这次用阿里云物联网平台时花了将近半天的时间在做之前做过又忘记怎么做的事情。本文就来介绍下消息解析物模型展示的功能。

另外阿里云免费的物联网平台公共实例的资源包将于2023年2月20日下线,我看企业版的最便宜的也要700元/月,我这种添加一个设备调试用的不能白嫖了,到时候需要的话只能包一台服务器自己搭个MQTT划算点了,或者看看其他云服务商那能不能白嫖😂。

添加物模型

设备管理->产品->对应产品名称->功能定义->编辑草稿中添加物模型数据,功能类型有属性、服务、事件,我目前只使用到了属性类型,编辑完成后发布上线即可,下面是我这次调试模块用到的物模型功能定义:

img

消息解析

设备管理->产品->对应产品名称->消息解析->编辑草稿中编写消息解析的脚本代码,有js、Python、php三种语言可供选择,我选择的是Python。消息解析有自定义Topic消息解析物模型消息解析两种,创建产品时数据格式选择透传/自定义,消息解析里才有设备上报数据和设备接收数据,数据格式选ICA 标准数据格式(Alink JSON),消息解析里只有自定义Topic消息解析。通过看模拟输入中模拟类型有哪些,可以知道是否支持某种消息类型的数据解析。

自定义Topic消息解析

设备通过携带解析标记?_sn=default的自定义Topic上报自定义格式消息时,物联网平台收到消息数据后,需调用消息解析脚本将自定义格式数据转换为JSON结构体,再流转给后续业务系统。例如,设备发送到Topic /${productKey}/${deviceName}/user/update的消息需要解析为JSON格式,在开发设备端时,就需配置该Topic为:/${productKey}/${deviceName}/user/update?_sn=default

在Python脚本中,自定义Topic消息解析的接口函数名为transform_payload(topic, rawData),可以根据不同的topic选择不同的解析方式。

物模型消息解析

数据格式为ICA标准数据格式,设备按照物联网平台定义的标准数据格式生成消息上报,标准Alink JSON数据格式说明,请参见设备属性、事件、服务

数据格式为透传/自定义,设备通信时,需要物联网平台调用您提交的消息解析脚本,将上行物模型消息解析为物联网平台定义的标准格式(Alink JSON),将下行物模型消息据解析为设备的自定义数据格式。

在Python脚本中,设备自定义数据格式转Alink JSON格式数据的函数(上行通信)为raw_data_to_protocol,Alink JSON格式数据转为设备自定义数据格式的函数(下行通信)为protocol_to_raw_data,要注意的是raw_data_to_protocol函数需要将rawData输入转为标准的Alink JSON,参考标准Alink JSON数据格式说明。下面是我这次用到的脚本解析代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ALINK_PROP_REPORT_METHOD = 'thing.event.property.post'
def raw_data_to_protocol(rawData):
uint8Array = []
for byteValue in rawData:
uint8Array.append(byteValue & 0xff)

jsonMap = {}
params = {}
params['status'] = uint8Array[0]
params['error'] = uint8Array[1]
params['validIDNum'] = uint8Array[2]
params['errorSlave'] = uint8Array[3]
params['idallocTimes'] = uint8Array[4]
params['costTime'] = (uint8Array[6]|(uint8Array[7]<<8)) # 非单字节变量注意大小端
params['successCnt'] = (uint8Array[8]|(uint8Array[9]<<8)|(uint8Array[10]<<16)|(uint8Array[11]<<24))
params['errorCnt'] = (uint8Array[12]|(uint8Array[13]<<8)|(uint8Array[14]<<16)|(uint8Array[15]<<24))
jsonMap['params'] = params # 物模型中的属性添加到params中,再加到jsonMap
jsonMap['method'] = ALINK_PROP_REPORT_METHOD # 标准的Alink JSON必须要加method

return jsonMap

物模型显示效果

这次应用是有软件模块过年放假期间需要测试,我用4G Cat.1模块传到阿里云物联网平台记录数据,最终物模型显示效果如下图:

img

printf函数

函数原型:int printf(const char *format, ...)
调用格式为:printf("<格式化字符串>", <参量表>);
功能:发送格式化输出到标准输出 stdout

变长参数实现思路

C语言支持变长参数函数(Variable Argument Functions),即参数的个数可以是不定个,在函数定义的时候用...表示。采用这种形式定义的变长参数函数,至少需要一个普通的形参,且...需要放在最后一个参数,比如printf函数中的*format后面的...是函数原型的一部分。

变长参数的实现得益于C语言默认的cdecl调用惯例,其参数是从右向左压入栈的,第一个参数位于栈顶。这样printf函数实现的时候,就无需关心调用他的函数会传递几个参数过来,而只要关心自己用到几个,将全部参数压入栈,函数处理时从栈中取即可。

自己实现一个变长参数的函数

C已经有现成可用的一些东西来帮我们实现变长参数,它主要通过stdarg.h头文件定义的一个变量类型(va_list)和三个宏(va_start、va_arg、va_end)来实现。

实现一个可变长参数的sum函数,第一个参数num传递变长参数中有参数的数量,紧接着后面会传递num个整型变量,具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int sum(int num, ...)
{
int i, val = 0;
va_list ap; //定义一个具有va_list型的变量,这个变量是指向参数的指针
va_start(ap, num); //始化变量刚定义的va_list变量,使其指向第一个可变参数的地址,地址自动增加
for(i = 0; i < num; i++)
{
val += va_arg(ap, int); //va_arg返回va_list中的参数,并增加指针偏移
}
va_end(ap); //结束可变参数列表
return val;
}
void main()
{
printf("16+38+53=%d\n", sum(3, 16, 38, 53));
}

变长参数实现原理

上面的sum函数也可以不使用va_list等宏,通过其他方法实现。
当我们调用:int n = sum(3, 16, 38, 53);
参数在栈上会形成如下布局:
img

在函数内部,函数可以使用变量num来访问数字3,但无法使用任何名称访问其他的几个不定参数。但此时由于栈上其他的几个参数实际恰好依序排列在参数num的高地址方向,因此可以很简单地通过num的地址计算出其他参数的地址,sum函数的另一种实现如下:

1
2
3
4
5
6
7
8
int sum(int num, ...)
{
int* p = &num + 1;
int ret = 0;
while(num--)
ret += *p++;
return ret;
}

printf的不定参数比sum要复杂得多,因为printf的参数不仅数量不定,而且类型也不定。所以printf需要在格式字符串中注明参数的类型,例如用%d表明是一个整数。printf里的格式字符串如果将类型描述错误,因为不同参数的大小不同,不仅可能导致这个参数的输出错误,还有可能导致其后的一系列参数错误。[摘自《程序员的自我修养——链接、封装、库》P338]

printf("%lf\t%d\t%c\n", 1, 666, 'a'); 在这行函数里,printf的第一个输出参数是一个int(4 字节),而我们告诉printf它是一个double(8字节),因此printf的输出会错误,由于printf在读取double的时候实际造成了越界,因此后面几个参数的输出也会失败。该程序的实际输出为:0.000000 97(根据实际编译器和环境可能不同)

va_list等宏如何实现

va_list 实际是一个指针,用来指向各个不定参数。由于类型不明,因此这个 va_list 以 void* 或 char* 为最佳选择。
va_start 将 va_list 定义的指针指向函数的最后一个参数后面的位置,这个位置就是第一个不定参数。
va_arg 获取当前不定参数的值,并根据当前不定参数的大小将指针移向下一个参数。
va_end 将指针清 0。
按照以上思路,va_list等宏的一个最简单的实现就可以得到了,如下所示:

1
2
3
4
#define va_list char*
#define va_start(ap, arg) (ap=(va_list)&arg+sizeof(arg))
#define va_arg(ap, t) (*(t*)((ap+=sizeof(t))-sizeof(t)))
#define va_end(ap) (ap=(va_list)0)

注意:实际代码中还套了很多宏,不同编译器,不同架构都有可能使用不同的代码实现,但具体实现思想一致,有些x64条件编译时va_list会是一个结构体,里面会记录可变参数开始地址、结束地址、参数数量等信息。