编辑
2025-04-21
记录知识
0

根据《GICv3中断简介》我们简单了解了gicv3中断控制器,这里根据《GICv3_Software_Overview_Official_Release_B.pdf》的Programmers’ model 来介绍一下gicv3的组成

整体划分

gicv3主要由三大部分组成,如下

  • Distributor interface
  • Redistributor interface
  • CPU interface

这里

  • Distributor:它检查中断源的状态,将SGI和SPI这类中断最终派发到到Redistributor上去,因为每个CPU都连接一个Redistributor,所以最终会派发到对应的CPU interface上
  • Redistributor:接收Distributor,然后最终将中断发送给CPU Interface
  • CPU interface: 就是我们常规理解的中断,它将中断发给CPU,让CPU响应中断

这里Distributor派发到Redistributor是根据优先级的,所以gicv3的affinity hierarchy 图如下

image.png

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

image.png

对于寄存器标识,如下

  • GICD_* : Distributor,也就是SGI/SPI的分发
  • GICR_* : Redistributor,接收中断发给GICC_* ,注意PPI的中断是直接给到GICR_* 不通过GICD_*
  • GICC_* : CPU interface, 发送给CPU,响应中断

根据上面的介绍,其实总结一下流程就是如下

  1. 外设发起中断,发送给distributor
  2. Distributor将该中断,然后根据优先等级分发给合适的Redistributor
  3. Redistributor将中断发送给CPU interface。
  4. CPU interface产生合适的中断异常给处理器
  5. CPU接收该异常,并且软件处理该中断

其中断状态机如下

image.png

  • Inactive: 不活跃状态
  • Pending:中断触发了,还没到CPU Interface,还没响应
  • Active:被CPU响应和处理
  • Active & Pending: (An instance of the interrupt has been acknowledged, and another instance is now pending. )当前有一个中断正在响应,此时有一个相同优先级的中断触发了

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

image.png

总结

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

image.png

编辑
2025-04-21
记录知识
0

在《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学艺不精。

编辑
2025-04-21
记录知识
0

在《一种特殊的栈破坏崩溃问题》中我们根据实际项目遇到的问题总结了一种破坏栈区导致的错误问题,对于此问题,上面文章总有点表述不清楚的感觉,本文基于基本知识点来梳理此问题,其目的是简单易懂的说明这个问题

值传递不超过16字节会使用x0-x7寄存器传值

首先我们需要知道aapcs中的如下表述,我们只需要看B.4

image.png

当符合类型超过16字节的时候,aapcs会启用x19保存加载内存的指针。那么此问题要出现,我们原始结构体应该是小于16字节的结构体。

那么需要具备条件的结构体如下

struct kernel{ int x; int y; int z; int s; };

aarch64的内存对齐是16字节

我们知道栈的对齐是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地址上

值传递不超过16字节时,默认分配栈区是24字节

但是我们看到的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的地址,程序访问出现段错误。

总结

本文更清晰的介绍了这个栈破坏的问题。助于理解

编辑
2025-04-18
记录知识
0

之前的内容已经能够在基于开源组件mosquitto 来进行mqtt的演示和利用了,但是一个产品功能并不是简单使用使用就完事儿了,所以需要开发。 而开发又分为两部分: 一个是协议栈开发,也就是针对官方协议文档去实现这份协议框架 另一个是应用开发,也就是针对协议栈进行产品应用场景的代码开发

这里我从网上找到了别人开发好的协议栈源码(cMQTT),在借助他人源码的基础之上,开发一个简单的应用程序,实现上述开源组件发布和订阅的基本功能,然后在此基础上衍生,大家一起探讨探讨,如何自己开发这个MQTT协议栈。

拉代码

git clone https://github.com/YorkJia/cMQTT.git

配置和编译

./confgure.sh && make install

这时候就会出现libcMQTT.so。这就可以利用它,去简单的编写一些代码了

编写发布者程序

  1. 创建文件
touch simple_pub.c
  1. 写个小例子
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)); }
  1. 开始编译
gcc simple_pub.c -I ../../infra/ -I ../../mqtt/ -L ../../ -lcMQTT -lpthread -o simple_pub
  1. 测试
mosquitto_sub -h 127.0.0.1 -t "test/a" -u kylin -P qwe123 ./simple_pub

可以看到mosquitto能够正常接收到hello world字串

image.png

至此,一个最简单的发布者代码已经编译完成了

编写订阅者程序

  1. 创建文件
touch simple_sub.c
  1. 写个小例子
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; } }
  1. 编译
gcc simple_sub.c -I ../../infra/ -I ../../mqtt/ -L ../../ -lcMQTT -lpthread -o simple_sub
  1. 测试 借用之前命令来发布主题消息
./simple_sub mosquitto_pub -h localhost -V mqttv31 -t 'test/a' -u kylin -P qwe123 -i "c3" -m "Hello World" -M 0

image.png

至此,一个最简单的订阅者代码已经编译完成了

将简单发布者和简单订阅者运行起来

  1. 运行Broker
mosquitto -c /etc/mosquitto/mosquitto.conf -d
  1. 运行示例程序
./simple_sub ./simple_pub

image.png

image.png

协议栈开发

说起协议栈的开发,与应用开发相比,一般都会是一个比较大的工程。就好像做菜。 应用开发者就好比去菜市场买菜,回家烹饪(或者点个外卖)。 协议栈开发者就好比从菜种子开始种,直到成熟后,再摘菜回家烹饪。

此文章不讨论实现MQTT的细节和展示协议栈实现的具体代码。而是和大家一起讨论讨论,怎么给菜园子松松土,把种子播进去。

代理(Broker)

从协议来看,代理的职责是接收订阅者的请求,将消息发送给订阅者,接收发布者的消息。 所以实现代理的方式应该大致如下:

image.png

其中,发布者和订阅者都是通过连接的方式接入Broker,epoll负责接收所有的事件数据,并解析和转发响应数据

订阅者

从协议来看,订阅者的职责是:设置QoS质量,选择需要订阅的主题,并将其封装成报文发送给代理去解析。然后回调接收订阅的消息。 所以实现订阅者的方式应该大致如下:

image.png

其中,回调处理主要为业务处理逻辑,如获取的温度,湿度,亮度等信息需要如何处理。 封装和解析的报文包括:CONNECT,CONNACK,SUBSCRIBE,SUBACK,UNSUBSCRIBE,UNSUBACK,PINGREQ,DISCONNECT

发布者

从协议来看,发布者的职责是:设置QoS质量,选择需要订阅的主题,并将其封装成报文发送给代理去解析。而发布者同时也可做订阅者,所以也可以回调接收订阅的消息 所以实现发布者(仅发布)的方式应该大致如下:

image.png

封装和解析的报文包括: 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定制协议

编辑
2025-04-18
记录知识
0

本文基于物联网来介绍一下MQTT

MQTT是什么?

MQTT:MQ Telemetry Transport,消息队列遥测传输协议。 它是非常轻量的消息传递协议,对于与需要较小代码占用和/或网络带宽非常宝贵的远程位置建立连接,它最有用。 MQTT是分布式的软总线,它的消息通过订阅者,发布者,消息代理三个角色实现,它能存在于所有的智能设备上。从而达到万物互联。当然实现物联网的物联网协议也很多,如CoAP,HTTP,UPnP,XMPP等,但MQTT具有更加简单,轻量的优势。

MQTT名词解释

  1. 订阅者(Subscriber)
    就是消息的接收者,设备需要消息,就从消息代理获取发布者推送的消息
  2. 发布者(Publisher)
    就是消息的发送者,它能提供给消息代理特定含义的消息
  3. 消息代理(Broker)
    就是接收订阅者和发布者的请求和消息,并根据请求转发消息
  4. 主题(Topic)
    就是发布者用于标识发布什么类型的消息,订阅者用于标识订阅什么类型的消息
  5. QoS
    就是用于确定发布者发布消息的等级,主要包括三个等级 qos0:表示未知服务,代表此消息最多发送一次,且不保证订阅者是否能够成功接收 qos1:表述已知服务,代表此消息发布后,需要得到至少一个订阅者的确定收到的回复,否则消息将被重发 qos2:表述保证服务,代表此消息发布后,不仅需要得到订阅者的回复,还会在收到订阅者回复后,再发送回复消息给订阅者确定。从而保证消息是完全稳定可靠的
  6. 临终遗嘱(Last Will and Testament) 就是当连接是异常断开的情况下,消息代理会按照发布者的临终遗嘱发送一条预设的消息。
  7. 保留消息(Retained)
    就是当订阅者订阅消息时,判断是否打开保留消息从而决定是否立即发送上一条消息给订阅者
  8. 树形结构(Tree Struct)
    就是指主题的分类按照树形结构分类,如卧室温度消息的结构为“home/houseroom/temperature”
  9. 通配符(+/#)
    就是可以通过通配符找到符合正则表达的所有主题,如 “home/houseroom/#”表示订阅所有卧室消息 “home/+/temperature”表示订阅所有房间的温度消息
  10. 清理会话(CleanSession)
    如果置位为0,已断开过的服务端必须基于当前会话状态恢复新的客户端连接 如果置位为1,必须丢弃之前的任何会话,并开始新的会话
  11. 客户端(Client)
    客户端包括订阅者和发布者,它会总是连接到MQTT服务器。 客户端可以:
  • (1)发布其他客户端可能会订阅的信息。
  • (2)订阅其它客户端发布的消息。
  • (3)退订或删除应用程序的消息。
  • (4)断开与服务器连接。
  1. MQTT服务器
    服务器就是消息代理Broker,它可以是一个应用程序,也可以是设备。它位于发布者和订阅者之间 服务器可以:
  • (1)接受来自客户的网络连接;
  • (2)接受客户发布的应用信息;
  • (3)处理来自客户端的订阅和退订请求;
  • (4)向订阅的客户转发应用程序消息。
  1. 保持连接(Keep Alive)
    就是指在客户端传输完成一个控制报文的时刻到发送下一个报文的时刻,两者最大允许的时间间隔。客户端需要保证控制报文发送时间不超过这个值。

消息发送

QoS0方式消息发送如下

image.png

QoS0级别的消息发送,意味着接收者并不会响应消息,发送者也不会做重试判断,所以消息最多可能送达一次,但也有可能无法送达 发送者:
发送这个 PUBLISH报文
接收者:
接收这个PUBLISH报文

QoS1方式发送

image.png

QoS1级别的消息发送,意味着消息至少被发送一次,并确定至少送达一次
发送者:
必须在每个新的消息上分配一个报文标识符,发送的PUBLISH报文必须标识为QoS=1,DUP=0,且必须将这个报文当作未确认的报文,直到接收到对应的响应报文
接收者:
响应PUBACK报文必须包含一个报文标识符,需要与接收到的PUBLISH报文相同
发送PUBACK报文后,接收者必须将任何包含相同报文标识符的PUBLISH报文当作一个新的消息。保证下一次仍能正常接收。

QoS2方式发送

image.png

QoS2是最高级别的发送方式,他确保消息不被丢失,同时也确保消息不重复。
发送者:
1.为消息分配一个未使用的报文标识符,然后将PUBLISH报文的标识符为置为QoS2 DUP0。
2.在发送PUBLISH报文时,将PUBLISH报文看作是未被确认的,直到收到PUBREC报文。且在收到PUBREC报文后必须发送一个PUBREL报文,该报文必须和PUBLISH报文具有相同的报文标识符。
3.同时,也必须将这个PUBREL报文看作是未确认的,直到从接收者那里收到对应的PUBCOMP报文。
4.最后,一旦发送了对应的PUBREL报文,就不能重发这个PUBLISH报文。

接收者:
1.响应的 PUBREC 报文必须包含报文标识符,这个标识符来自接收到的、已经接受所有权的PUBLISH 报文。
2.在收到对应的 PUBREL 报文之前,接收者必须发送 PUBREC 报文确认任何后续的具有相同标识符的 PUBLISH 报文。 在这种情况下,它不能重复分发消息给任何后续的接收者。
3.响应 PUBREL 报文的 PUBCOMP 报文必须包含与 PUBREL 报文相同的标识符。
4.发送 PUBCOMP 报文之后,接收者必须将包含相同报文标识符的任何后续 PUBLISH 报文当作一个新的发布。

测试验证

在Linux环境上体验体验MQTT

  1. 安装mosquitto
apt update && apt install mosquitto mosquitto-clients
  1. 创建认证文件
touch /etc/mosquitto/pwfile

创建用户和密码

mosquitto_passwd /etc/mosquitto/pwfile kylin

密码 qwe123

  1. 设置ACL规则
vim /etc/mosquitto/aclfile user kylin topic write test/# topic read test/#
  1. 启动消息代理(Broker)
mosquitto -c /etc/mosquitto/mosquitto.conf -d
  1. 订阅一个主题
mosquitto_sub -h localhost -t "test/a" -u kylin -P qwe123 -i "c1" -h 为指定mqtt的host地址 -t 为指定mqtt的主题 -u 为用户 -P 为密码 -i 为process id
  1. 发布一个主题
mosquitto_pub -h localhost -V mqttv311 -t 'test/a' -u kylin -P qwe123 -i "c3" -m "Hello World" -M 0 其中-h -t -u -i -P 意义与上一致 -V 指定发布消息的版本(mqttv311/mqttv31) -m 为发布的消息字符串 -M 为指定QoS等级

使用tcpdump抓数据包

image.png

如上图可以看到,在订阅者这里会阻塞轮询消息,直到发布者发送消息,订阅者就能收到消息“Hello World”

那么,随即而来就会产生一个疑问。之前将了这么多概念,那实际上这是怎么实现的呢?
探索如何实现这个事情,就需要利用tcpdump了
TCP抓包:

tcpdump -i lo tcp port 1883 -X

-i: 代表网络接口
tcp: mqtt是基于tcp传输的
port 1883: mqtt默认传输端口为1883
-X:在抓包时,以十六进制和 ASCII 表示打印每个数据包的数据

上面为完整的数据包内容,鉴于TCP有三次握手,所以取第四个数据包来简单解析MQTT数据包。如下

image.png

因为这是一个完整的TCP数据包,所以需要去掉一些TCP协议相关的数据 其中

4500 0053 8998 4000 4006 b30a 7f00 0001 7f00 0001 :为TCP的报文头。不具体分析 ae80 075b 6e6a c507 dded ec9e 8018 0200 fe47 0000 0101 080a 265c 053f 265c 053f :传输控制协议报文信息。不具体分析 101d 0004 4d51 5454 04c2 003c 0002 6333 0005 6b79 6c69 6e00 0671 7765 3132 33 :为真正的TCP数据包,也就是MQTT的数据包。主要分析这块数据

分析MQTT数据包

101d: 0x10由协议表:2.2.1可以查到,对应CONNECT,0x1d为字节长度,这里计算为29个字节

image.png

0004 4d51 5454 04c2 003c:
0004 :协议名字长度为4
4d51 5454:名字为MQTT(ASCII码)(如果是3.1协议,则字串为MQIsdp(4d51 4973 6470),长度为6)
04:版本号:3.1.1版本号为4, 3.1版本号为3。
c2:连接标志,包含:名称,密码,QoS0,清理会话。(3.1.2.3章节)

image.png

003c:保持连接时间,默认为60秒。超时情况下会发送PINGREQ报文用于探测broker和client(发布者和订阅者)直接是否仍在线(3.1.2.10章节)

image.png

0002 6333: 0002为Client ID 长度为2,6333 ID为字符串c3(也就是process ID),我发布的时候用-i参数指定了c3
0005 6b79 6c69 6e00 0671 7765 3132 33: 这里为账户kylin 密码 qwe123的字符串明文

总结

至此,一个完整的connect包已经解析完成了。在connect之后,其实后面还有许多数据包都能进行解析。