淘宝买小电影怎么搜索:自己动手编写嵌入式Bootloader之(2)

来源:百度文库 编辑:偶看新闻 时间:2024/04/28 03:07:10
第二部分:通过网口下载内核映像
要实现通过网口下载文件的功能,从底层到上层需要做的工作包括:开发板上的网卡芯片的驱动程序;TCP/IP协议栈的实现;TFTP客户端应用程序的实现。我们使用的OK2440开发板配备CS8900A网卡芯片。 为了简单起见,网络数据包的发送和接收都使用轮询方式,不使用中断;协议栈只使用ARP/IP/UDP协议,不涉及TCP及其他协议;应用程序只实现最简单的TFTP客户端。
1. 全局配置信息
发送和接收的数据缓冲区,使用全局静态缓冲区,不使用动态内存分配。第一阶段运行结束之后,CPU内部4KB的SteppingStone可以用作其它用途,我们就用它做网络数据接收、发送的缓冲区。亦可用作标准输入输出的缓冲区。
unsigned char *TxBuf = (unsigned char *)0;
unsigned char *RxBuf = (unsigned char *)1024;
使用若干个全局变量来保存网络配置信息:
unsigned char    NetOurEther[6] =            /* Our ethernet address        */
{0x00, 0x09, 0x58, 0xD8, 0x11, 0x22};
开发板的MAC地址,这个是任意设置的。
unsigned char    NetServerEther[6] =            /* Boot server enet address    */
{0x00, 0x14, 0x2A, 0xA5, 0x50, 0x97};
服务器也就是主机的MAC地址,这个要跟主机MAC一致,可以在主机上运行ifconfig命令查到。
unsigned long    NetOurIP = 0xC0A801FC;        /* Our IP addr 192.168.1.252    */
unsigned long    NetServerIP = 0xC0A801F9;       /* Server IP   192.168.1.249    */
网络协议中IP地址一般是用一个4字节整型数表示的。
2. CS8900A以太网驱动程序
硬件电路决定了CS8900的物理地址是在BANK3的区间内,CS8900是16位的寄存器,故我们设置BANK3的BUS WIDTH也为16位。设置BANK3: 总线宽度16,使能nWait,使能UB/LB
BANKCON3:0x1F7C
网卡CS8900的访问基址为0x19000000,之所以再偏移0x300是由它的特性决定的
#define CS8900_BASE 0x19000300
CS8900 读写寄存器的方式有些特别。要读一个寄存器,先向CS8900_PPTR中写入该寄存器地址,再从CS8900_PDATA中读出该寄存器值;要写一个寄存器,先向CS8900PPTR中写入该寄存器地址,再向CS8900_PDATA中写入要写入的值。不管是寄存器地址还是要读写的数值,都是16位的,也就是说都是unsigned short类型的。因此,读写寄存器的函数如下:
static unsigned short get_reg (int regno)
{
CS8900_PPTR = regno;
return CS8900_PDATA;
}
static void put_reg (int regno, unsigned short val)
{
CS8900_PPTR = regno;
CS8900_PDATA = val;
}
读芯片ID: CS8900的芯片ID存放在PP_ChipID寄存器中,读该寄存器得到的正确值应该是0x630E,这可以初步判断一些地址/引脚的设置是否正确,如果读出的不是0x630E,那么CS8900肯定不能正常工作。
设置MAC地址:
MAC地址并不是固定的,可以由我们随意设置。从寄存器PP_IA开始的6个字节存放MAC地址。比如下面的代码把MAC地址设为 00 09 58 D8 11 22:
put_reg (PP_IA + 0, 0x00 | 0x09 << 8);
put_reg (PP_IA + 2, 0x58 | 0xD8 << 8);
put_reg (PP_IA + 4, 0x11 | 0x22 << 8);
因为是Little Endian, 所以0x09<<8, 但是在寄存器内存中还是 0x00放在前面。
寄存器初始化: 设置CS8900的工作模式
/* 只接收目标地址为本网卡的无错误数据包 */
put_reg (PP_RxCTL, PP_RxCTL_IA | PP_RxCTL_Broadcast | PP_RxCTL_RxOK);
/* 当进行接收操作时,不要产生任何中断 */
put_reg (PP_RxCFG, 0);
/* 当进行发送操作时,不要产生任何中断 */
put_reg (PP_TxCFG, 0);
/* 当进行缓存操作时,不要产生任何中断 */
put_reg (PP_BufCFG, 0);
/* 使能发送和接收模式 */
put_reg (PP_LineCTL, PP_LineCTL_Rx | PP_LineCTL_Tx);
发送数据包:
int eth_send (volatile void *packet, int length)
两个参数:要发送的数据包首地址、长度
TxCMD 和TxLen寄存器用来初始化数据包的发送,其具体含义见CS8900数据手册第70页。这里PP_TxCmd_TxStart_Full被定义为 0x00C0,表示直到整个数据侦都加载到CS8900内部缓存之后才开始发送,数据侦的长度为CS8900_TxLEN.
/* initiate a transmit sequence */
CS8900_TxCMD = PP_TxCmd_TxStart_Full;
CS8900_TxLEN = length;
使用TxCMD下达发送数据的命令后,再读取 PP_BusSTAT 总线状态寄存器判断是否做好发送数据的准备。当get_reg (PP_BusSTAT) & PP_BusSTAT_TxRDY 不等于零时表示可以发送了。 使用一个循环进行实际的发送操作:
for (addr = packet; length > 0; length -= 2)
{
CS8900_RTDATA = *addr++;
}
这里 addr 也是unsigned short类型的指针, 每次向CS8900_RTDATA写入两个字节数据。这里假设要发送的数据包长度为偶数。
最后,通过读取PP_TER寄存器可以知道是否发送完毕,是否发送成功。
接收数据包:
首先,通过读取PP_RER寄存器判断是否接收到数据。如果接收到数据,则连续两次读取 CS8900_RTDATA 的值,
status = CS8900_RTDATA;        /* stat */
rxlen = CS8900_RTDATA;        /* len */
rxlen 为接收到的数据长度。
然后用一个循环连续读取 rxlen 长度的数据:
for (addr = (unsigned short *) &RxBuf[0], i = rxlen >> 1; i > 0;
i--)
*addr++ = CS8900_RTDATA;
if (rxlen & 1)
*addr++ = CS8900_RTDATA;
其中 RxBuf 为预先在内存中开辟的一块接收缓冲区。 每次循环读取两个字节,还需要处理长度为奇数的情况。
最后,把RxBuf交给上层的协议处理:net_receive( &RxBuf[0], rxlen );
3. Ethernet MAC层协议的实现
上层的数据包(如IP包、ARP包)到来时,需要添加一个14字节的MAC头, 然后再交给网卡发送出去。 MAC头包含目的MAC地址、源MAC地址、协议类型三个字段。如下图所示。数据包末尾的CRC校验我们不使用。

使用下面的代码填充MAC头。其中协议类型,对IP为0x0800, 对ARP为0x0806
struct mac_header *p = (struct mac_header*)(buf);
memcpy (p->dest, NetServerEther, 6);
memcpy (p->src, NetOurEther, 6);
p->proto = htons(proto);
4. ARP协议的实现
一般的方式是建立一个全局的ARP映射缓存表,随着系统的运行不断查找、更新该表。但是我们要完成的功能仅仅是从TFTP服务器下载内核和文件系统映像,而服务器的IP和MAC地址都是固定的,因此可以简化ARP映射表,只用两个变量分别保存服务器IP和MAC,再用两个变量保存开发板IP和MAC即可。并且更新映射表的功能也可以省略,只在系统初始化时把这四个地址都设置好,使用过程中不会发生改变,所以不需要更新。这样,我们的ARP协议只需要完成接受ARP请求、发送ARP应答的功能,而发送ARP请求和接受ARP应答的功能可以省略,这样大大简化了协议栈的设计。
按照维基百科上的介绍(http://en.wikipedia.org/wiki/Address_Resolution_Protocol),ARP 是一个数据链路层协议,(我感觉它应该是网络层的协议),它的作用是在只知道一个主机网络层IP地址的情况下找到它的硬件地址。在以太网上,它主要用来把 IP地址转换为以太网MAC地址。由于是链路层协议,ARP的作用范围仅限于本地局域网。
ARP数据包长度为28字节,其中各字节的含义如下图所示:

对各个段作简单的解释:
Hardware type (HTYPE)  每个数据链路层协议都被分配到一个数,比如,Ethernet 是 1
Protocol type (PTYPE)  在这个域,每个网络层协议都被分配到一个数(标号),比如,IP是0x0800
Hardware length (HLEN)  硬件地址的长度。以太网Ethernet的MAC地址长度是6个字节
Protocol length (PLEN)  维基上写的是“逻辑地址”的长度,其实也就是网络层地址的长度。IPv4地址的长度为4个字节。
Operation  表明发送者的操作,也就是数据包的类型:1表示ARP请求;2表示ARP回应;3表示RARP请求;4表示RARP回应。
Sender hardware address (SHA)  发送者的硬件地址
Sender protocol address (SPA)  发送者的协议地址,也就是发送者IP地址。
Target hardware address (THA)  目标接收者的硬件MAC地址。如果是ARP请求,这个域被忽略。
Target protocol address (TPA)  目标接收者的IP地址。
知道了包结构,我们就可以设计一个结构体:
struct arp_header{
unsigned short        ar_hrd;        /* Format of hardware address    */
unsigned short        ar_pro;        /* Format of protocol address    */
unsigned char        ar_hln;     /* Length of hardware address    */
unsigned char        ar_pln;     /* Length of protocol address    */
unsigned short        ar_op;        /* Operation            */
unsigned char        ar_sha[6];    /* Sender hardware address    */
unsigned long        ar_spa;     /* Sender protocol address    */
unsigned char        ar_tha[6];    /* Target hardware address    */
unsigned long        ar_tpa;     /* Target protocol address    */
}__attribute__ ((packed));
属性 __attribute__((packet)) 告诉编译器使用紧缩方式存放结构体内容(1 Byte align), 不使用默认的4字节对齐, 这样就不会产生冗余字节。此时的 sizeof(struct arp_header) = 28。 如果不加packed属性, 运行 sizeof(struct arp_header) 得到 32, 而不是 28。 数据段就产生了错位。
前面已经说过,我们只实现接收ARP请求并发送ARP应答的功能,因此只用一个简单的函数就可实现:
static int arp_handle( unsigned char *buf, unsigned int len )
{
struct arp_header *pRx, *pTx;
pRx = (struct arp_header *)(buf);
pTx = (struct arp_header *)&TxBuf[256];
switch (htons(pRx->ar_op))
{
case ARP_REQUEST:
if (pRx->ar_tpa == htonl(NetOurIP))
{
pTx->ar_hrd = htons(0x01);
pTx->ar_pro = htons(PROTO_IP);
pTx->ar_hln = 0x06;
pTx->ar_pln = 0x04;
pTx->ar_op = htons(ARP_REPLY);
memcpy(pTx->ar_sha, NetOurEther, 6);
pTx->ar_spa = htonl(NetOurIP);
memcpy (pTx->ar_tha, pRx->ar_sha, 6);
pTx->ar_tpa = pRx->ar_spa;
mac_send( (unsigned char*)pTx, sizeof(struct arp_header), PROTO_ARP);
}
break;
case ARP_REPLY:
printf("\n\rGot ARP reply\n");
break;
default:
printf("\n\r ar_op Not Support.\n");
break;
}
return 0;
}
接收到的数据保存在pRx地址处,要发送的数据地址指定为pTx位于发送缓冲区中。如果接收到的是ARP请求包并且IP地址也符合,则在pTx处构造一个ARP应答包并交给mac_send()发送出去。
5. IP协议的实现
IP数据包的格式如下表所示:
+
Bits 0–3
4–7
8–15
16–18
19–31
0
Version
Header length
Type of Service
Total Length
32
Identification
Flags
Fragment Offset
64
Time to Live
Protocol
Header Checksum
96
Source Address
128
Destination Address
160
Options
160 or 192+
Data
IP协议的简化:IP协议在网络中主要完成路由选择和网络分段的功能。起始Bit 0-3表示版本号,对IPv4来说取值为4即0100即可。Header length域指明IP数据包header的长度(不包括数据Data域),以四字节为单位,因为Options域是可选的所以IP Header的长度并不固定。我们不使用Option域,所以取最小值5,表示Header长度为20字节。服务类型域(Type of Service, TOS)是为特殊的应用如VoIP等保留的,我们不使用,赋值为零即可。接下来2个字节的Total Length域表示整个数据包的长度,包括Header和Data,以字节为单位。 标识域(Identification)用来给数据包一个唯一的编号,用于验证和跟踪等,我们不使用,直接赋值为零即可。Flags和Offset用于分段包的重组,我们不使用,把Flags的第2位设为1表示是不可分段的,Offset赋值为零即可。生存时间(Time to Live, TTL)表示该数据包在网络上的有效期,我们简单的把它设为最大值0xFF即可。协议域(Protocol)表示传输层使用什么协议,RFC790文档为每个协议都规定了唯一的编号,如UDP编号为17。Header Checksum为Header区域的校验和,在校验之前该域初始为0,然后计算整个头部的校验和,把结果存放在该域,计算校验的方法是把头部看成以16位为单位的数字组成,依次进行二进制反码求和。接下来的八个字节是源IP地址和目的IP地址,没什么可说的。
综上所述,我们只保留了IP协议中必须的关键字段,因而简化了设计,对IP数据包进行填充的代码段如下:
struct ip_header *p = (struct ip_header*)(buf);
p->ver_ihl = 0x45;                  // 1 Byte
p->tos = 0x00;                      // 1 Byte
p->tlen = htons(len);               // 2 Byte
p->identification = htons(0x00);    // 2 Byte
p->flags_fo = htons(0x4000);        // 2 Byte
p->ttl = 0xFF;                      // 1 Byte
p->proto = 17;                      // 1 Byte, 17 for UDP
p->ip_src = htonl(NetOurIP);        // 4 Byte
p->ip_dest = htonl(NetServerIP);    // 4 Byte
p->crc = 0x0;                       // 2 Byte, To be
p->crc = checksum( buf, sizeof(struct ip_header) );
CheckSum 校验和:
IP,TCP,UDP等许多协议的头部都设置了校验和项,它们采用的算法是一样的,将被校验的数据按16位进行划分(若数据字节长度为奇数,则在数据尾部补一个字节0),对每16位求反码和,然后再对和取反码。 代码如下:
unsigned short checksum(unsigned char *ptr, int len)
{
unsigned long sum = 0;
unsigned short *p = (unsigned short *)ptr;
while (len > 1)
{
sum += *p++;
len -= 2;
}
if(len == 1)
sum += *(unsigned char *)p;
while(sum>>16)
sum = (sum&0xffff) + (sum>>16);
return (unsigned short)((~sum)&0xffff);
}
6. UDP协议的实现
bits 0 - 15 16 - 31
0 Source Port Destination Port
32 Length Checksum
64
Data
在传输层我们抛弃了复杂的TCP协议而使用简单的UDP协议。虽然UDP是无连接的协议,它不保证数据包一定能够到达目的主机,但是在嵌入式开发中,开发板跟主机通常位于同一内部局域网内,网络环境良好,数据丢失的可能性很小,并且UDP容易实现,占用资源小,因此更适合于嵌入式环境。 UDP头部包含了可选的校验和字段,而校验要涉及到伪报头,为了简化设计和减小开销,我们不使用校验,直接把该字段设为零,表示不使用校验。UDP包填充代码如下:
struct udp_header *P = (struct udp_header*)(buf);
P->port_src = htons(0x8DA4); // 2 Byte
P->port_dest = htons(port);  // 2 Byte
P->tlen = htons(len);        // 2 Byte
P->crc = 0x00;               // Do Not Checksum, 2 Byte
关于源端口号和目的端口号的设定,在TFTP实现时会详细说明。
7. TFTP客户端的实现
tftp是一个很简单的文件传输协议,在传输层使用UDP协议。它有四种类型的包: 读请求RRQ包,DATA包,ACK包,ERROR包,每个包的前两个字节Opcode指定包的类型。(RRQ用于请求下载,WRQ用于请求上传,我们只用到RRQ)。

下载文件的过程分析如下: 客户端(A)从任意端口X向服务器(S)的端口69发送一个RRQ包,该包中指明了要求下载的文件名;服务器(S)找到该文件,读取文件内容组成DATA包,从任意端口Y向客户端(A)的端口X发送这个DATA包,第一个DATA包编号为1;从此以后,客户端确定使用端口X,服务器确定使用端口Y, 客户端向服务器发送ACK包,编号为1。服务器接到编号为1的ACK包之后,发送第二个DATA包,如此继续下去。
怎样判断传输结束呢? 按照规定,DATA包中的数据段为512字节, 如果小于512字节,表示这是最后一个DATA包,文件已传输完毕。

(R1) Host A requests to read

(R2) Server S sends data packet 1

(R3) Host A acknowledges data packet 1
注意在这个过程中端口的变化。开始RRQ是69,但是DATA和ACK都不是使用69,而是使用另外一个随机的端口。 服务器在接到RRQ后,不返回任何回应信息,直接发送第一个DATA包,而且DATA包编号从1开始,而不是从0开始。
编程时为简单起见,客户端使用了固定的端口号X=0x8DA4,服务器端口号Y是随机的,只能通过解析UDP数据包获得。
int tftp_download(unsigned char *addr, const char *filename)
{
int i=0;
unsigned short curblock = 1;
tftp_send_request( &TxBuf[256], filename );
msdelay(100);
while (1)
{
eth_rx();
if( pGtftp == NULL )
continue;
if ( ntohs(pGtftp->opcode) == TFTP_DATA )
{
if (ntohs(pGtftp->u.blocknum) == curblock)
{
printf("\r Current Block Number = %d", curblock);
for (i=0; i{
*(addr++) = *(pGtftp->data+i);
}
tftp_send_ack( &TxBuf[256], curblock);
if (iGLen < TFTP_DATASIZE+4)
{
break;
}
curblock += 1;
}
else if (ntohs(pGtftp->u.blocknum) < curblock)
{
tftp_send_ack( &TxBuf[256], ntohs(pGtftp->u.blocknum));
}
else
{
printf("\n\rBlock Number Not Match.");
printf("Block Number = %d, curblock = %d\n", ntohs(pGtftp->u.blocknum), curblock);
}
}
else if ( ntohs(pGtftp->opcode) == TFTP_ERROR )
{
switch( ntohs(pGtftp->u.errcode) )
{
// 此处省略
}
}
else if ( ntohs(pGtftp->opcode) == TFTP_RRQ )
{}// 此处省略若干 else if
pGtftp = NULL;
iGLen = 0;
}
printf("\n\rTransfer complete: %d Bytes.\n\r", (curblock-1)*TFTP_DATASIZE + iGLen-4 );
return 0;
}
http://blog.chinaunix.net/u/7459/showart_2022532.html
第三部分:源代码,运行结果
这一部分将对前文没有提到的几段关键代码进行简单说明,介绍一下源代码组织结构和Makefile系统,展示一下实验运行结果,并提供全部源代码下载。
1. 定时器初始化和延时程序
因为在 CS8900A的驱动程序中需要用到延时,因此有必要对S3C2440的计时器进行使能和初始化,并编写延时程序。
S3C2440A共有5个定时器,编号为Timer0 ~ Timer4。其中Timer0 ~ Timer3都有输出引脚,可以通过定时器来控制引脚电平周期性的变化,这称为脉冲宽度调制(PWM:Pulse Width Modulation)功能。而Timer4没有输出引脚,也就没有PWM功能,所以Timer4常被程序里的延时函数使用。
定时器部件的时钟源为PCLK,但是需要经过两级预分频之后才真正供定时器使用。第一级预分频由TCFG0寄存器控制,其位[7:0]设置预分频器0的值,供Timer0和Timer1使用,位[15:8]设置预分频器1的值,供Timer2 ~ Timer4使用。第二级预分频由TCFG1寄存器控制,其每四位控制一个定时器,可以从2分频、4分频、8分频、16分频、外接TCLK0/TCLK1 这五种频率中选择。
我们的延时函数使用Timer4,其它定时器全部关闭。初始化程序中设置:TCFG0 = 0x0f00; 表示Timer4的第一级预分频值为 15+1 = 16。寄存器TCFG1使用默认值全0,表示第二级预分频为2分频。前面已经设置PCLK为50MHz,这样Timer4实际的工作频率为:
50MHz/16/2 = 50000000/32 = 1562500Hz
注意计算时钟频率时的MHz是指10^6,而不是2^20;同理KHz是指1000Hz,而不是1024Hz。
我们在TCON中把Timer4设为”自动加载“。当Timer4启动时,TCNTB4的值将被自动装入内部寄存器TCNT4,然后在工作频率下,TCNT4开始减1计数,当到达0时,TCNTB4的值又被自动装入TCNT4,下一个计数流程开始。我们把TCNTB4设为15625,则一个计数流程的的长度为10毫秒。
假设要延时的时间为msec毫秒,则共需要的计数值为 tmo = msec*15625/10,设一个变量timestamp保存已经过去的时间戳,每次读取TCNT4的值后更新timestamp,直到它大于 tmo 。程序如下:
while( timestamp < tmo )
{
thisdec = TCNTO4 & 0xffff;
if( lastdec >= thisdec )   /* normal mode */
{
timestamp += lastdec - thisdec;
}
else          /* we have an overflow ... */
{
timestamp += lastdec + TIMER_LOAD_VAL - thisdec;
}
lastdec = thisdec;
}
TCNT4的值可由寄存器TCNTO4读出。程序中保存了最近两次读出的TCNTO4值, 如果本次值比上次小,说明在同一个计数流程内;如果本次值比上次大,说明已经进入了下一个计数流程。
2. 串口标准输入输出
要想在Bootloader中使用scanf()和print()并不容易,因为不能直接使用C库函数。scanf()要从串口获得输入, print()要向串口进行输出。必须自己实现常用的C库函数, 不仅包括输入输出函数,还包括字符串操作函数如strcmp(), strcpy()等。幸好在《嵌入式Linux应用开发完全手册》这本书的源代码中提供了这样简化的C库,所以就直接拿来用了。
代码中定义了两个全局数组作为输入输出缓冲区:
static unsigned char g_pcOutBuf[ 1024 ];
static unsigned char g_pcInBuf[ 1024 ];
其实我们可以把这两个缓冲区定位在CPU的 SteppingStone 里面,这样可以节省2K的空间。
scanf()的实现里面调用 getc() 函数, printf() 的实现里面调用 putc() 函数。我们自己写getc()函数为从串口读取字符, putc()函数实现为向串口发送字符, 这样标准输入输出就跟串口联系在一起了。
/* 发送一个字符 */
void putc(unsigned char c)
{
/* 等待,直到发送缓冲区中的数据已经全部发送出去 */
while (!(UTRSTAT0 & TXD0READY));
/* 向UTXH0寄存器中写入数据,UART即自动将它发送出去 */
UTXH0 = c;
}
/* 接收字符 */
unsigned char getc(void)
{
unsigned char ret;
/* 等待,直到接收缓冲区中的有数据 */
while(!(UTRSTAT0 & RXD0READY));
/* 直接读取URXH0寄存器,即可获得接收到的数据 */
ret = URXH0;
if (ret == 0x0d || ret == 0x0a)
{
putc(0x0d);
putc(0x0a);
}
else
{
putc(ret);
}
return ret;
}
3. 源代码组织结构
源代码跟目录下只有两个文件, 主Makefile和链接脚本sboot.lds。
文件夹start内有start.S和nand.c,前者是上电后最初运行的汇编代码,后者含有Nand Flash的读函数,负责把S-Boot代码从Nand拷贝到RAM中。
文件夹main内有main.c,是一个死循环,提供若干菜单供用户选择,然后调用相应功能的程序。
文件夹lib内是简化和移植过的C标准库,包括输入输出和字符串操作函数。
文件夹include内是一些头文件。
文件夹app内有boot_linux.c和tftp.c,从名字就能看出它们的功能。
文件夹device内含有设备驱动程序,如串口初始化、定时器初始化和延时函数、网卡驱动、网络协议实现等。
每个文件夹内都有自己的Makefile,根目录下的主Makefile会进入各个子目录并调用各自的Makefile。每个子目录下的Makefile把自己编译的代码链接成一个build-in.o文件, 主Makefile把各个子目录下的build-in.o链接成一个可执行文件。
编译器使用自己制作的 arm-hwlee-linux-gnueabi-gcc.可以从这里下载。  给gcc增加 -nostdinc 选项, 表示不使用标准C库函数,不到/usr/include目录下寻找包含文件, 只在-I$(INCLUDEDIR)指定的目录寻找包含文件。
4. 提供全部源代码下载:
文件: S-Boot.tar.gz
大小: 41KB
下载:下载
5. 运行结果截图

图中,首先选择3从TFTP服务器下载内核到RAM中, 然后选择4从RAM成功启动内核。
选择2还有通过串口Kermit协议下载内核的功能,前文没有对这部分代码作分析,有时间再补上。下面附一张截图:

http://blog.chinaunix.net/u/7459/showart_2022660.html