这样的一个系统其执行过程如下图所示:
图 5. 系统工作流程Ktelnet 在启动后,生成和维护一个 CLI (命令行)界面。它利用这个界面接收用户输入的命令,再将命令按上一节描述的 ktelnet 协议格式封装成 ICMP 报文发送给 ktelnetd 。
ktelnted 接收来自 ktelnet 的报文,将从报文中解析出的命令提交给 kshell 。 kshell 维护着一个命令列表。这个列表中记录了命令名称和相应的执行函数。 kshell 根据 ktelnetd 提交的命令,调用相应的函数,完成这个命令的执行任务。执行的结果以字符串的形式保存在内存中。 ktelnetd 在命令执行完毕后,生成一个 ICMP_ECHOREPLY 报文,将执行结果封装到报文中,回应给 ktelnet 。
ktelnet 从收到的报文中解析出执行结果,并将结果输出在 CLI 界面上。由于命令和执行结果都是以字符串的形式表现的,因此,在整个过程中,用户的使用感受与 telnet 服务一样。
在通常的应用环境中,ktelnet 应基于 Windows 。即使是 Linux 开发人员也往往使用 Windows 做为客户端。
ktelnet 首先需要初始化 winsock 库,创建一个原始( RAW )的 socket 。
1
2
3
4
5
6
7
8
9
10
11
12
| WSADATA wsd;
SOCKET s;
/* init windows socket libarary */
if (WSAStartup(MAKEWORD(2,2), &wsd)!=0)
perror ("init fail\n");
if ((s = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP)) < 0)
{
perror("ktelnet: socket fail\n");
return -1;
}
|
然后,输出一个提示符(#),等待用户输入命令。
1
2
3
4
5
6
7
8
9
10
| char cmd[128], c;
printf("#"); /* print prompt */
i = 0;
memset(cmd, 0, sizeof(cmd));
while ( (c=getchar()) != '\n' && (i < (sizeof(cmd)-1)))
{
cmd[i++] = c;
}
|
当用户结束命令的输入按下回车键后,保存在字符数组 cmd 中的数据需要被封装到 ICMP 报文中发送出去。我们定义了下面所示的 ICMP 头部。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| struct icmphdr {
__u8 type;
__u8 code;
__u16 checksum;
union {
struct {
__u16 id;
__u16 sequence;
} echo;
__u32 gateway;
struct {
__u16 __unused;
__u16 mtu;
} frag;
} un;
};
|
利用这个头部结构,我们可以构造出一个 ICMP_ECHO 类型的报文,再将用户输入的命令存入这个报文的净载荷中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| struct icmphdr *icp;
struct sockaddr whereto;
icp = (struct icmphdr *)buff;
icp->type = ICMP_ECHO; /* TYPE */
icp->code = 0;
icp->checksum = 0;
icp->un.echo.id = 0x400; /* ID */
/* compute ICMP checksum here */
icp->checksum = in_cksum((u_short *)icp, size);
payload = buff + sizeof(struct icmphdr); /* payload of icmp */
payload[0] = 'L'; /* magic */
payload[1] = 'X';
*((short *)(payload+2)) = htons(strlen(cmd)); /* length */
strcpy(&payload[4], cmd); /* command */
i = sendto(s, (char *)buff, size, 0, &whereto, sizeof(struct sockaddr));
|
ktelnetd 执行了命令后,执行结果仍使用 ICMP 报文发送回来。 ktelnet 需要解析这些回应报文。
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
| struct iphdr {
__u8 ihl:4,
version:4;
__u8 tos;
__u16 tot_len;
__u16 id;
__u16 frag_off;
__u8 ttl;
__u8 protocol;
__u16 check;
__u32 saddr;
__u32 daddr;
/*The options start here. */
};
struct iphdr *ip;
/* Check the IP header */
ip = (struct iphdr *)buff;
hlen = ip->ihl << 2; /* head length */
payload = buff + hlen + sizeof(struct icmphdr);
/* check magic */
if (payload[0] != 'L' || payload[1] != 'X')
return -1;
printf("%s", payload+4); /* print result */
|
将命令的执行设计为一个逻辑模块是为了便于理解和方便未来命令的扩展。 kshell 维护了一个命令列表。这个列表的结构为:
1
2
3
4
5
6
| typedef void (*KSHELL_CMD)(char *cmd, char *r, int size);
struct {
char name[128];
KSHELL_CMD fn;
}kshell_cmd_set[32];
|
name 中保存了命令名称,而 fn 则是这个命令的处理函数。一个典型的预置命令是 type 。它的语法是:
这个命令的处理函数 kshell_cmd_type 将根据 addres 参数取出该地址的数据。数据被转换为字符串存入全局变量中。
将 type 命令加入列表中的方法是:
1
2
| strcpy(Kshell_cmd_set[0].name, “ type ” );
kshell_cmd_set[0].fn = kshell_cmd_type;
|
命令列表记录了各个命令的名称和处理函数。只需要在这个列表中增加新的命令,kshell 的功能就得到了扩充。只要 kshell 向外部暴露一个扩展接口,开发人员就可以将自己设计的命令功能添加到 kshell 中。 kshell 的设计目标就是为了便于扩展。
ktelnetd 被植入 ICMP 协议栈,因此,它的代码是分散的。主要集中在 icmp_rcv 函数和 icmp_echo 函数中。
首先,ktelnetd 需要在 icmp_rcv 函数中过滤 ICMP 报文。当发现带有 magic 的 ICMP_ECHO 报文时, ktelnetd 从报文中过滤出 ktelnet 请求执行的命令。
下面的代码放置在代码 icmp_pointers[icmph->type].handler(skb); 之前。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| #define KTELNET_HEAD_SIZE 4
/* filter ktelnet request */
if (icmph->type == ICMP_ECHO && \
skb->data[0] == 'L' && skb->data[1] == 'X'){
cmd_len = ntohs(*(short *)(skb->data+2)); /* command length */
memset(ktelnet_cmd, 0, sizeof(ktelnet_cmd));
strncpy(ktelnet_cmd, skb->data+KTELNET_HEAD_SIZE, cmd_len);
/* execute the command */
l = kshell_main(ktelnet_cmd); /* execute and save result */
ktelnet_req = 0;
ktelnet_req = 1;
}
|
我们定义的私有协议 ktelnet 的报文头部是 4 个字节。首先检查头部中的 magic 。然后就可以获得命令的长度信息,最后,将过滤出的命令保存在变量 ktelnet_cmd 中。通过 kshell_main 函数这个变量被提交给 kshell 。
在 kshell_main 函数中,kshell 检索 kshell_cmd_set[32] 列表,找到命令对应的处理函数,并调用函数。在处理函数的执行过程中,命令执行结果被存入全局变量 ktelnet_result 中。
在命令被成功执行之后,ktelnetd 置下全局标志 ktelnet_req 。这样,icmp_echo 函数就可以了解到当前正在处理的报文是 ktelnet 协议。
在 icmp_echo 函数的一开始,就执行下面的 ktelnetd 代码:
1
2
3
4
5
6
7
8
9
10
11
| /* set magic */
skb->data[0] = 'L';
skb->data[1] = 'X';
*(short *)(skb->data+2) = strlen(ktelnet_result); /* set length */
/* copy result */
strcpy(skb->data + KTELNET_HEAD_SIZE, ktelnet_result);
/* clear mark */
ktelnet_req = 0;
|
命令的执行结果 ktelnet_result 被封装到 SKB 结构中,icmp_echo 函数最后调用 icmp_reply 函数将回应报文发送出去。
在植入了 ktelnetd 后,开发人员通过 ktelnet 客户端可以接入嵌入式设备的内核。开发人员也可以根据自己的需要扩展各种命令,诊断或调试内核。前文曾提及命令 type 。以此命令为例,开发人员在客户端的控制台上执行:
1
2
3
4
5
| C:\>ktelnet 192.168.1.1
#
#type 0xc051dc40
1
#
|
0xc051dc40 是内核全局变量 num_processors 的地址,表示处理器数量。这个数据可以通过 system.map 查询得到。
从这个例子可以看出 ktelnetd 将帮助开发人员获得访问内核的能力。通过对 kshell 支持命令的扩展,我们将得到更加丰富的功能。
此外,实验也表明,当一个 I/O 驱动进行无限循环之类消耗 CPU 的行为时,ktelnetd 仍可以正常工作。这无疑有助于开发人员的定位和诊断。 |