根据《GICv3中断简介》我们简单了解了gicv3中断控制器,这里根据《GICv3_Software_Overview_Official_Release_B.pdf》的Programmers’ model 来介绍一下gicv3的组成
gicv3主要由三大部分组成,如下
这里
这里Distributor派发到Redistributor是根据优先级的,所以gicv3的affinity hierarchy 图如下

所以,我们知道了gicv3的整体框图如下

对于寄存器标识,如下
根据上面的介绍,其实总结一下流程就是如下
其中断状态机如下

不过LPI中断没有active 或 active and pending 状态,也就是上面的中断状态机只针对SPI,SGI,PPI的。所以LPI的状态机如下

根据上面的信息,我们中间gic-v3的寄存器调用如下图

在《glibc内存malloc简要解析》就介绍了glibc的一些概念,简单来说就是将malloc返回的地址的前16字节叫做chunk header。
在我们操作系统中,遇到了一个非常奇怪的问题,那就是调用xcb的程序,总是在退出的时候,莫名其妙报glibc的错误,包含但不局限于如下
malloc_consolidate(): unaligned fastbin chunk detected double free or corruption (!prev) corrupted double-linked list malloc(): memory corruption
今天介绍这个问题
static lazyreply *get_index(xcb_connection_t *c, int idx) { if(c->ext.extensions_size < 0) c->ext.extensions_size = 0; if(idx > c->ext.extensions_size) { int new_size = idx << 1; lazyreply *new_extensions = realloc(c->ext.extensions, sizeof(lazyreply) * new_size); if(!new_extensions) return 0; memset(new_extensions + c->ext.extensions_size, 0, sizeof(lazyreply) * (new_size - c->ext.extensions_size)); c->ext.extensions = new_extensions; c->ext.extensions_size = new_size; } return c->ext.extensions + idx - 1; }
这里对于extensions_size是小于0的情况,强制置0
if(c->ext.extensions_size < 0) c->ext.extensions_size = 0;
我们可以看到get_index的代码,这里会将ext.extensions进行realloc,realloc之后,将新增的大小区域进行memset为0。那么问题就出现在memset上了。
memset(new_extensions + c->ext.extensions_size, 0, sizeof(lazyreply) * (new_size - c->ext.extensions_size));
我们假设extensions_size是-1,那么memset就会清空realloc申请的内存的chunk head结构体。
chunk的数据内容被清空了,那么程序可能正常,也可能在glibc的回收,规整,free,分配等操作中都会出现异常
根据这个问题现象,他是一个非常随机的问题,存在此问题的系统,会给人感觉系统非常不稳定。因为xcb的应用只要运行,就会随机出错,或者大概率退出的时候出错。而且出错的日志全部指向了glibc的内存管理。
而实际上此问题就是对某个内存地址,错误的将chunk head清空了导致的。
定位这个问题也比较困难,我们需要先了解glibc的内存管理相关逻辑,然后gdb找到崩溃现场。根据glibc的逻辑,他是使用双向循环链表来管理每个chunk的bin,所以推荐使用pwndbg工具,这个工具在定位链表上非常方便。
实际上,为了找到xcb的问题,我重编了glibc,mesa,xcb,dri,xorg。甚至我还找rk拿了mali so的符号版本。
同样的,为了前期排查问题,还使用了asan来寻找内存问题。当然,浪费时间了,自己asan学艺不精。
在《一种特殊的栈破坏崩溃问题》中我们根据实际项目遇到的问题总结了一种破坏栈区导致的错误问题,对于此问题,上面文章总有点表述不清楚的感觉,本文基于基本知识点来梳理此问题,其目的是简单易懂的说明这个问题
首先我们需要知道aapcs中的如下表述,我们只需要看B.4

当符合类型超过16字节的时候,aapcs会启用x19保存加载内存的指针。那么此问题要出现,我们原始结构体应该是小于16字节的结构体。
那么需要具备条件的结构体如下
struct kernel{ int x; int y; int z; int s; };
我们知道栈的对齐是16字节,实际上,对于值传递寄存器保存的值,也需要16字节对齐。 我们对test函数反汇编,那么如下
In file: /tmp/test.c:33 28 } 29 30 void test(struct kernel k) 31 { 32 struct user* s = (struct user*)&k; ► 33 s->o4 = 3; 34 test1(s); 35 return ; 36 } 37 38 int main(int argc, char *argv[]) ───────────────────────────────────────────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────────────────────────────────────────── 00:0000│ x29 sp 0x7ffffff2f0 —▸ 0x7ffffff320 —▸ 0x7ffffff380 ◂— 0 01:0008│ 0x7ffffff2f8 —▸ 0x4006e8 (main+108) ◂— mov w0, #0 02:0010│ x0 0x7ffffff300 ◂— 0x20000000b /* '\x0b' */ 03:0018│ 0x7ffffff308 ◂— 3 04:0020│ 0x7ffffff310 ◂— 1 05:0028│ 0x7ffffff318 —▸ 0x7ffffff300 ◂— 0x20000000b /* '\x0b' */ 06:0030│ 0x7ffffff320 —▸ 0x7ffffff380 ◂— 0
我们看到栈顶是0x7ffffff2f0,那么先保存x29和x30之后,其他剩下的寄存器需要保存值的起始地址应该是0x7ffffff300
所以,我们知道k变量的默认地址在0x7ffffff300,因为k的大小是16字节,那么0x7ffffff310应该就是下一个局部变量的地址 。0x7ffffff308是k结构体的结束。如果k的结构体不是16字节,那么也会按照16字节对齐到0x7ffffff310地址上
但是我们看到的0x7ffffff310并不是下一个局部变量的地址,而0x7ffffff318是下一个局部变量s的地址
这里的原因是,从0x7ffffff310到0x7ffffff318共8个字节。
这里gcc的实现故意在此情况添加了8个字节的padding。
也就是说,默认情况下,值传递不超过16字节时,那么默认分配的栈来保存寄存器值的空间是16+8等于24字节
我们知道0x7ffffff318的后面是下一个局部变量的分配地址,那么0x7ffffff318就是s的地址。
那么如果正常访问,从k的地址0x7ffffff300开始,只能访问
0x7ffffff300/0x7ffffff304/0x7ffffff308/0x7ffffff30c
那么如果想要访问到0x7ffffff318,那么需要再访问3个4字节,所以,假设强制类型转换为
struct user{ int x; int y; int z; int s; int o1; int o2; int o3; };
此时访问s->o3,则会访问到0x7ffffff318地址,而0x7ffffff318地址又是指针s在栈区的地址,那么破坏了s的地址,程序访问出现段错误。
本文更清晰的介绍了这个栈破坏的问题。助于理解
之前的内容已经能够在基于开源组件mosquitto 来进行mqtt的演示和利用了,但是一个产品功能并不是简单使用使用就完事儿了,所以需要开发。 而开发又分为两部分: 一个是协议栈开发,也就是针对官方协议文档去实现这份协议框架 另一个是应用开发,也就是针对协议栈进行产品应用场景的代码开发
这里我从网上找到了别人开发好的协议栈源码(cMQTT),在借助他人源码的基础之上,开发一个简单的应用程序,实现上述开源组件发布和订阅的基本功能,然后在此基础上衍生,大家一起探讨探讨,如何自己开发这个MQTT协议栈。
git clone https://github.com/YorkJia/cMQTT.git
配置和编译
./confgure.sh && make install
这时候就会出现libcMQTT.so。这就可以利用它,去简单的编写一些代码了
touch simple_pub.c
int main(int argc, char *argv[]) { int res, loop_cnt = 0, cnt = 0; mqtt_client_t *pclient = NULL; pclient = mqtt_client_new("127.0.0.1", 1883, NULL, "client/01", "kylin", "qwe123"); mqtt_set_will_opt(pclient, MQTT_QOS0, 0, "test/a", NULL); do{ printf("try to connect broker...\n"); res = mqtt_client_connect(pclient); sleep(1); }while(res != SUCCESS_RETURN); while (1) { example_publish(pclient, "Hello World"); mqtt_client_yield(pclient, 1000); } mqtt_client_close(pclient);} int example_publish(mqtt_client_t *pclient, char* data) { char payload[100]; if(pclient == NULL){ return FAIL_RETURN; } memset(payload, 0, sizeof(payload)); sprintf(payload, data); mqtt_publish_simple(pclient, "test/a", MQTT_QOS0, payload, strlen(payload)); }
gcc simple_pub.c -I ../../infra/ -I ../../mqtt/ -L ../../ -lcMQTT -lpthread -o simple_pub
mosquitto_sub -h 127.0.0.1 -t "test/a" -u kylin -P qwe123 ./simple_pub
可以看到mosquitto能够正常接收到hello world字串

至此,一个最简单的发布者代码已经编译完成了
touch simple_sub.c
int main(int argc, char *argv[]) { int res, loop_cnt = 0, cnt = 0; mqtt_client_t *pclient = NULL; pclient = mqtt_client_new("127.0.0.1", 1883, NULL, "client/02", "kylin", "qwe123"); mqtt_set_will_opt(pclient, MQTT_QOS0, 0, "test/a", NULL); do{ printf("try to connect broker...\n"); res = mqtt_client_connect(pclient); sleep(1); }while(res != SUCCESS_RETURN); while (1) { res = mqtt_subscribe(pclient, "test/a", MQTT_QOS0, example_message_arrive, NULL); if(res < 0){ printf("subscribe[%s] fail.\n", "test/a"); } mqtt_client_yield(pclient, 1000); } mqtt_client_close(pclient); } void example_message_arrive(void *pcontext, void *pclient, mqtt_event_msg_t *msg) { mqtt_topic_info_t *topic_info = (mqtt_topic_info_t *)msg->msg; switch (msg->event_type) { case IOTX_MQTT_EVENT_PUBLISH_RECEIVED: /* print topic name and topic message */ printf("Message Arrived:\n"); printf("Topic : %.*s\n", topic_info->topic_len, topic_info->ptopic); printf("Payload: %.*s\n", topic_info->payload_len, topic_info->payload); break; default: break; } }
gcc simple_sub.c -I ../../infra/ -I ../../mqtt/ -L ../../ -lcMQTT -lpthread -o simple_sub
./simple_sub mosquitto_pub -h localhost -V mqttv31 -t 'test/a' -u kylin -P qwe123 -i "c3" -m "Hello World" -M 0

至此,一个最简单的订阅者代码已经编译完成了
mosquitto -c /etc/mosquitto/mosquitto.conf -d
./simple_sub ./simple_pub


说起协议栈的开发,与应用开发相比,一般都会是一个比较大的工程。就好像做菜。 应用开发者就好比去菜市场买菜,回家烹饪(或者点个外卖)。 协议栈开发者就好比从菜种子开始种,直到成熟后,再摘菜回家烹饪。
此文章不讨论实现MQTT的细节和展示协议栈实现的具体代码。而是和大家一起讨论讨论,怎么给菜园子松松土,把种子播进去。
从协议来看,代理的职责是接收订阅者的请求,将消息发送给订阅者,接收发布者的消息。 所以实现代理的方式应该大致如下:

其中,发布者和订阅者都是通过连接的方式接入Broker,epoll负责接收所有的事件数据,并解析和转发响应数据
从协议来看,订阅者的职责是:设置QoS质量,选择需要订阅的主题,并将其封装成报文发送给代理去解析。然后回调接收订阅的消息。 所以实现订阅者的方式应该大致如下:

其中,回调处理主要为业务处理逻辑,如获取的温度,湿度,亮度等信息需要如何处理。 封装和解析的报文包括:CONNECT,CONNACK,SUBSCRIBE,SUBACK,UNSUBSCRIBE,UNSUBACK,PINGREQ,DISCONNECT
从协议来看,发布者的职责是:设置QoS质量,选择需要订阅的主题,并将其封装成报文发送给代理去解析。而发布者同时也可做订阅者,所以也可以回调接收订阅的消息 所以实现发布者(仅发布)的方式应该大致如下:

封装和解析的报文包括: CONNECT,CONNACK,PUBLISH,PUBACK,PUBREC,PUBREL,PUBCOMP,PINGREQ,DISCONNECT
这里列举需要实现的协议特性:
1.主题设置:合理的设置主题和判断主题
2.主题过滤器:合理的使用通配符
3.会话管理:Broker合理的管理和调度众多的会话连接
4.保持连接:合理的判断连接是否持续
5.临终遗嘱:合理的执行遗嘱内容,包括主题,消息
6.响应:合理的设置响应,如服务端在合理的时间内收不到connect报文,应该主动管理会话
7.Qos等级:合理的运用QoS设置报文发送方式
8.清理会话:合理的保留和遗弃上一次会话的消息
9.保留消息:合理的判断是否保留上一次发送的消息
至此,我们演示了MQTT的应用场景,我们既需要基于MQTT开发应用,又需要根据MQTT定制协议