10

逸珺 · 2020年04月08日

深度解析U-Boot网络实现

1.U-Boot网络架构分析

TCP/IP OSI model

下图比较清楚的描述TCP/IP模型与OSI 七层模型的对应关系:

-TCP-IP-stack.jpg

对于U-Boot而言,并没有完整的实现上述模型,u-boot需要控制固件的尺寸,所以根据需要做了一些简化,其拓扑框架如下图所示:

注:这样分层绘制,仅为理解方便,按OSI模型是否严谨不是本文重点。

image-20200322131712460.png

网络通讯的总调度接口位于./net.c中int net_loop(enum proto_t protocol)。

u-boot循环主题位于./common中的main.c

void main_loop(void)
{
    const char *s;
    bootstage_mark_name(BOOTSTAGE_ID_MAIN_LOOP, "main_loop");

#ifdef CONFIG_VERSION_VARIABLE
    setenv("ver", version_string);  
#endif /* CONFIG_VERSION_VARIABLE */
    /*初始化命令行解释器*/
    cli_init();
    
    /*从环境变量中获取"preboot"的定义
     *该变量包含了一些预启动命令,一般环
     *境变量中不包含该项配置
     */
    run_preboot_environment_command();

    /*如果使能了CONFIG_UPDATE_TFTP
     *则自动从TFTP服务器更新文件 
     */
#if defined(CONFIG_UPDATE_TFTP)
    update_tftp(0UL, NULL, NULL);
#endif /* CONFIG_UPDATE_TFTP */
    
    /*从环境变量中取出"bootdelay"
    *和"bootcmd"的配置值,将取出的
    *"bootdelay"配置值转换成整数,
    *处理延时引导*/
    s = bootdelay_process();
    if (cli_process_fdt(&s))
        cli_secure_boot_cmd(s);

    autoboot_command(s);
    
    /*延时引导过程中有控制台输入,
    *进入命令行解释器*/
    cli_loop();
    panic("No CLI available");
}

在cli_loop中各依据输入的命令做出判断,如果输入命令验证是合法输入命令(格式参数语法正确校验通过)则进入上述网络框图中的应用层进行处理。其入口点是net_loop函数。

对框图中的基本协议进行总结:

  • NFS:网络文件系统,英文Network File System(NFS),是由SUN公司研制的UNIX表示层协议(presentation layer protocol),能使使用者访问网络上别处的文件就像在使用自己的计算机一样。NFS是基于UDP/IP协议的应用,其实现主要是采用远程过程调用RPC机制,RPC提供了一组与机器、操作系统以及低层传送协议无关的存取远程文件的操作。RPC采用了XDR的支持。XDR是一种与机器无关的数据描述编码的协议,他以独立与任意机器体系结构的格式对网上传送的数据进行编码和解码,支持在异构系统之间数据的传送。
  • DNS:域名系统(服务)协议(DNS)是一种分布式网络目录服务,主要用于域名与 IP 地址的相互转换,以及控制因特网的电子邮件的发送。
  • BOOTP:BOOTP(Bootstrap Protocol,引导程序协议)是一种引导协议,基于UDP/IP协议,也称自举协议,是DHCP协议的前身。BOOTP用于无盘工作站的局域网中,可以让无盘工作站从一个中心服务器上获得IP地址。通过BOOTP协议可以为局域网中的无盘工作站分配动态IP地址,这样就不需要管理员去为每个用户去设置静态IP地址。
  • PING:PING (Packet Internet Groper),因特网包探索器,用于测试网络连接量的程序 [1] 。Ping是工作在 TCP/IP网络体系结构中应用层的一个服务命令, 主要是向特定的目的主机发送 ICMP(Internet Control Message Protocol 因特网报文控制协议)Echo 请求报文,测试目的站是否可达及了解其有关状态 [2] 。
  • TFTP:TFTP(Trivial File Transfer Protocol,简单文件传输协议)是TCP/IP协议族中的一个用来在客户机与服务器之间进行简单文件传输的协议,提供不复杂、开销不大的文件传输服务。端口号为69。TFTP是一个传输文件的简单协议,它基于UDP协议而实现。此协议设计的时候是进行小文件传输的。因此它不具备通常的FTP的许多功能,它只能从文件服务器上获得或写入文件,不能列出目录,不进行认证,它传输8位数据。传输中有三种模式:netascii,这是8位的ASCII码形式,另一种是octet,这是8位源数据类型;最后一种mail已经不再支持,它将返回的数据直接返回给用户而不是保存为文件。
  • SNTP:简单网络时间协议(Simple Network Time Protocol),由 NTP 改编而来,主要用来同步因特网中的计算机时钟。在 RFC2030 中定义。SNTP协议采用客户端/服务器的工作方式,可以采用单播(点对点)或者广播(一点对多点)模式操作。SNTP服务器通过接收GPS信号或自带的原子钟作为系统的时间基准。单播模式下,SNTP客户端能够通过定期访问SNTP服务器获得准确的时间信息,用于调整客户端自身所在系统的时间,达到同步时间的目的。广播模式下,SNTP服务器周期性地发送消息给指定的IP广播地址或者IP多播地址。SNTP客户端通过监听这些地址来获得时间信息。
  • UDP:Internet 协议集支持一个无连接的传输协议,该协议称为用户数据报协议(UDP,User Datagram Protocol)。UDP 为应用程序提供了一种无需建立连接就可以发送封装的 IP 数据包的方法。RFC 768 [1] 描述了 UDP。UDP 是User Datagram Protocol的简称, 中文名是用户数据报协议,是OSI(Open System Interconnection,开放式系统互联) 参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务,IETF RFC 768 [1] 是UDP的正式规范。UDP在IP报文的协议号是17。
  • LINK-LOCAL:链路本地地址(Link-local address),又称连结本地位址是计算机网络中一类特殊的地址, 它仅供于在网段,或广播域中的主机相互通信使用。这类主机通常不需要外部互联网服务,仅有主机间相互通讯的需求。IPv4链路本地地址定义在169.254.0.0/16地址块。 IPv6定义在fe80::/10地址块。
  • ARP:地址解析协议,即ARP(Address Resolution Protocol),是根据IP地址获取物理地址的一个TCP/IP协议。主机发送信息时将包含目标IP地址的ARP请求广播到局域网络上的所有主机,并接收返回消息,以此确定目标的物理地址;收到返回消息后将该IP地址和物理地址存入本机ARP缓存中并保留一定时间,下次请求时直接查询ARP缓存以节约资源。地址解析协议是建立在网络中各个主机互相信任的基础上的,局域网络上的主机可以自主发送ARP应答消息,其他主机收到应答报文时不会检测该报文的真实性就会将其记入本机ARP缓存;由此攻击者就可以向某一主机发送伪ARP应答报文,使其发送的信息无法到达预期的主机或到达错误的主机,这就构成了一个ARP欺骗。ARP命令可用于查询本机ARP缓存中IP地址和MAC地址的对应关系、添加或删除静态对应关系等。相关协议有RARP、代理ARP。NDP用于在IPv6中代替地址解析协议。
  • RARP:反向地址转换协议(RARP:Reverse Address Resolution Protocol) 反向地址转换协议(RARP)允许局域网的物理机器从网关服务器的 ARP 表或者缓存上请求其 IP 地址。网络管理员在局域网网关路由器里创建一个表以映射物理地址(MAC)和与其对应的 IP 地址。当设置一台新的机器时,其 RARP 客户机程序需要向路由器上的 RARP 服务器请求相应的 IP 地址。假设在路由表中已经设置了一个记录,RARP 服务器将会返回 IP 地址给机器,此机器就会存储起来以便日后使用。 RARP 可以使用于以太网、光纤分布式数据接口及令牌环 LAN。

    ARP(地址解析协议)是设备通过自己知道的IP地址来获得自己不知道的物理地址的协议。假如一个设备不知道它自己的IP地址,但是知道自己的物理地址,网络上的无盘工作站就是这种情况,设备知道的只是网络接口卡上的物理地址。这种情况下应该怎么办呢?RARP(逆地址解析协议)正是针对这种情况的一种协议。
    RARP以与ARP相反的方式工作。RARP发出要反向解析的物理地址并希望返回其对应的IP地址,应答包括由能够提供所需信息的RARP服务器发出的IP地址。虽然发送方发出的是广播信息,RARP规定只有RARP服务器能产生应答。许多网络指定多个RARP服务器,这样做既是为了平衡负载也是为了作为出现问题时的备份。

  • PHY:PHY(英语:Port Physical Layer),中文可称之为端口物理层,是一个对OSI模型物理层的共同简称。PHY连接一个数据链路层的设备(MAC)到一个物理媒介,如光纤或铜缆线。典型的PHY包括PCS(Physical Coding Sublayer,物理编码子层)和PMD(Physical Media Dependent,物理介质相关子层)。PCS对被发送和接受的信息加码和解码,目的是使接收器更容易恢复信号。

2.协议栈工作机制

2.1 以nfs为例

NFS的实现命令入口位于./cmd/net.c (注:不同版本U-Boot文件名不一样,有的版本叫cmd_net.c) ,U_BOOT_CMD创建的do_nfs实现函数,该函数会调用netboot_common(enum proto_t proto, cmd_tbl_t cmdtp, int argc,char const argv[]) 调用会话层的具体实现。proto_t 为协议类型枚举变量:BOOTP, RARP, ARP, TFTPGET, DHCP, PING, DNS, NFS, CDP, NETCONS, SNTP,TFTPSRV, TFTPPUT, LINKLOCAL。不同的应用协议传入不同的协议类型,以NFS为例:

  • netboot_common(NFS, cmdtp, argc, argv);

    对应NFS 的调用接口与函数实现就容易理解了

    nfs [loadAddress] [[hostIPaddr:]bootfilename]

从层级调用的角度分析,会话层nfs.c收到该请求,将依据NFS协议向下层发送UDP报文请求:net_send_udp_packet。同时将接收处理回调函数udp_packet_handler设置为nfs接收报文处理函数。net_send_udp_packet该函数的原型为:

int net_send_udp_packet(uchar *ether, struct in_addr dest, int dport, int sport,int payload_len)

  • ether:NFS服务器以太网MAC地址,即开发主机的MAC地址
  • struct in_addr dest:NFS服务器IP地址,即开发主机的IP地址
  • int sport:端口号
  • int payload_len:有效载荷长度

传输层将上层的发送请求用net_send_packet发送至设备层:

inline void net_send_packet(uchar *pkt, int len)

  • uchar *pkt:报文指针
  • int len:长度

设备层通过eth_send调用驱动层的具体发送函数,这取决于是采用代理模式或通用设备框架,见物理层实现章节描述。设备层接收到底层设备驱动的接收报文请求调用eth_rx(),将接收报文返回给传输层。

函数原型为:int eth_rx(void)

int eth_rx(void)
{
    struct udevice *current;
    uchar *packet;
    int flags;
    int ret;
    int i;

    current = eth_get_dev();
    if (!current)
        return -ENODEV;

    if (!device_active(current))
        return -EINVAL;

    /* Process up to 32 packets at one time */
    flags = ETH_RECV_CHECK_DEVICE;
    for (i = 0; i < 32; i++) {
        ret = eth_get_ops(current)->recv(current, flags, &packet);
        flags = 0;
        if (ret > 0)
            net_process_received_packet(packet, ret);
        if (ret >= 0 && eth_get_ops(current)->free_pkt)
            eth_get_ops(current)->free_pkt(current, packet, ret);
        if (ret <= 0)
            break;
    }
    if (ret == -EAGAIN)
        ret = 0;
    if (ret < 0) {
        /* We cannot completely return the error at present */
        debug("%s: recv() returned error %d\n", __func__, ret);
    }
    return ret;
}

int eth_rx将调用net_process_received_packet处理报文解析,如果报文IP头部校验通过,将调用udp_packet_handler对应的接收处理回调函数,进行NFS应用协议的处理。

这样就完成了完整的收发通信过程。

2.2 报文封装

报文封装与拆包大略示意图如下:

image-20200322135020665.png

自上而下,数据传入下一层将加上该层的头部:

IP/UDP头部

struct ip_udp_hdr {
    /*IP 头部*/
    u8        ip_hl_v;    /* header length and version    */
    u8        ip_tos;        /* type of service        */
    u16        ip_len;        /* total length            */
    u16        ip_id;        /* identification        */
    u16        ip_off;        /* fragment offset field    */
    u8        ip_ttl;        /* time to live            */
    u8        ip_p;        /* protocol            */
    u16        ip_sum;        /* checksum            */
    struct in_addr    ip_src;        /* Source IP address        */
    struct in_addr    ip_dst;        /* Destination IP address    */
    
    /*以下为UDP头部*/
    u16        udp_src;    /* UDP source port        */
    u16        udp_dst;    /* UDP destination port        */
    u16        udp_len;    /* Length of UDP packet        */
    u16        udp_xsum;    /* Checksum            */
};

3.物理层实现

U-Boot网络架构的物理层的主要职责是负责网络报文的收发。控制以太网收发芯片工作,并将二进制数据流接收发送。如上图所示,其底层驱动抽象剥离实现于./drivers/net,该层主要与设备抽象层交互,主要与eth_legacy.c或者eth-uclass.c交互。而设备抽象层通过net_send_packet服务原语提供对上层的发送报文服务,报文接收主要由net_process_received_packet服务原语提供物理层接收报文服务,U-Boot为简化设计,物理层的接收函数直接在net_loop函数解析。依据接收的报文类型再做出后续的处理动作。eth_common主要实现物理MAC地址读与改写等操作,并实现将当前设备接入系统的操作,由函数eth_set_current实现。

设备抽象层主要由eth_legacy.c或者eth-uclass.c实现,这两个文件位于./net下:

  • 当使能设备管理框架宏时CONFIG_DM_ETH时,使用eth-uclass.c与底层设备驱动进行交互。此时网络底层设备采用udevice通用设备框架进行交互。
  • 当关闭设备管理框架宏时CONFIG_DM_ETH时,使用eth_legacy.c与底层设备驱动进行交互。此时网络底层驱动程序通过代理模式实现具体底层驱动与上层应用进行交互,实现了对上层的接口统一,以及对底层设备驱动差异化的兼容代理。
  • eth_legacy.c和eth-uclass.c 都实现了eth_set_dev接口,将设备根据环境变量ethprime,将网络设备设置为系统当前的主网络设备。

3.1 eth_legacy.c 代理模式

eth_legacy.c 对外的设备接口为 eth_device结构体,该结构体对设备的基本属性进行抽象封装:

image-20200320010150755.png

eth_device 以太网设备接口,抽象了底层设备的以下主要属性以及基本操作:

  • char name[16]:字符串命名,如"RTL8139#%d
  • unsigned char enetaddr[6]:48位 MAC地址
  • phys_addr_t iobase: 以太网收发芯片IO基址
  • int state:设备状态机,ETH_STATE_INIT/ETH_STATE_PASSIVE/ETH_STATE_ACTIVE三种状态
  • int (init)(struct eth_device , bd_t *):设备初始化接口
  • int (send)(struct eth_device , void *packet, int length):设备发生报文接口
  • int (recv)(struct eth_device ):设备接收报文接口
  • void (halt)(struct eth_device ):设备停止处理接口
  • int (mcast)(struct eth_device , const u8 *enetaddr, u8 set):多播接口
  • int (write_hwaddr)(struct eth_device ):写MAC地址接口,有的芯片内置MAC地址存储器,故可选地需要实现这个接口。
  • ...

对于设备驱动如以代理模式工作,只需要根据芯片DATASHEET实现上述接口,并调用int eth_register(struct eth_device dev)接口将该结构体操作符注册进入代理设备框架,即实现了设备驱动的移植。当底层设备收到报文时,./net中void net_process_received_packet(uchar in_packet, int len)会自动通过eth_device设备操作接口完成数据接收以及解析。

那么对于设备层驱动在什么函数进行设备注册呢?以常见的RTL8139芯片为例,实现rtl8139_initialize,将rtl8139模块内部上述操作的局部接口函数初始化eth_device变量,同时将该结构体变量注册挂载进代理框架就完成了设备的挂载。

int rtl8139_initialize(bd_t *bis)
{
    pci_dev_t devno;
    int card_number = 0;
    struct eth_device *dev;
    u32 iobase;
    int idx=0;

    while(1){
        /* Find RTL8139 */
        if ((devno = pci_find_devices(supported, idx++)) < 0)
            break;

        pci_read_config_dword(devno, PCI_BASE_ADDRESS_1, &iobase);
        iobase &= ~0xf;
        debug ("rtl8139: REALTEK RTL8139 @0x%x\n", iobase);
        dev = (struct eth_device *)malloc(sizeof *dev);
        if (!dev) {
            printf("Can not allocate memory of rtl8139\n");
            break;
        }
        memset(dev, 0, sizeof(*dev));
        sprintf (dev->name, "RTL8139#%d", card_number);

        dev->priv = (void *) devno;
        dev->iobase = (int)bus_to_phys(iobase);
        dev->init = rtl8139_probe;
        dev->halt = rtl_disable;
        dev->send = rtl_transmit;
        dev->recv = rtl_poll;
#ifdef CONFIG_MCAST_TFTP
        dev->mcast = rtl_bcast_addr;
#endif
        eth_register (dev);

        card_number++;
        pci_write_config_byte (devno, PCI_LATENCY_TIMER, 0x20);

        udelay (10 * 1000);
    }
    return card_number;
}

3.2 udevice 框架模式

eth-uclass.c对设备驱动层进行操作抽象封装,驱动程序需要实现操作符eth_ops:

  • int (start)(struct udevice dev):启动设备
  • int (send)(struct udevice dev, void *packet, int length):发送报文
  • int (recv)(struct udevice dev, int flags, uchar **packetp):接收报文
  • int (free_pkt)(struct udevice dev, uchar *packet, int length):释放报文
  • void (stop)(struct udevice dev):停止设备
  • int (mcast)(struct udevice dev, const u8 *enetaddr, int join):多播
  • int (write_hwaddr)(struct udevice dev):写MAC操作
  • int (read_rom_hwaddr)(struct udevice dev):读ROM MAC地址操作

2.png

以mcs7830芯片为例:

/*操作函数实现略*/
/*1.实现操作符eth_ops mcs7830_eth_ops以及对应的操作函数*/
static const struct eth_ops mcs7830_eth_ops = {
    .start    = mcs7830_eth_start,
    .send    = mcs7830_eth_send,
    .recv    = mcs7830_eth_recv,
    .free_pkt = mcs7830_free_pkt,
    .stop    = mcs7830_eth_stop,
    .write_hwaddr = mcs7830_write_hwaddr,
};
/*2.利用U_BOOT_DRIVER定义BOOT driver*/
U_BOOT_DRIVER(mcs7830_eth) = {
    .name    = "mcs7830_eth",
    .id    = UCLASS_ETH,
    .probe = mcs7830_eth_probe,
    .ops    = &mcs7830_eth_ops,
    .priv_auto_alloc_size = sizeof(struct mcs7830_private),
    .platdata_auto_alloc_size = sizeof(struct eth_pdata),
    .flags    = DM_FLAG_ALLOC_PRIV_DMA,
};
/*3.定义ID表*/
static const struct usb_device_id mcs7830_eth_id_table[] = {
    { USB_DEVICE(0x9710, 0x7832) },        /* Moschip 7832 */
    { USB_DEVICE(0x9710, 0x7830), },    /* Moschip 7830 */
    { USB_DEVICE(0x9710, 0x7730), },    /* Moschip 7730 */
    { USB_DEVICE(0x0df6, 0x0021), },    /* Sitecom LN 30 */
    { }        /* Terminating entry */
};
/*4.定义BOOT_USB设备*/
U_BOOT_USB_DEVICE(mcs7830_eth, mcs7830_eth_id_table);
  1. 实现操作符eth_ops mcs7830_eth_ops以及对应的操作函数
  2. 利用U_BOOT_DRIVER定义BOOT driver
  3. 定义ID表
  4. 定义BOOT_USB设备

对于U_BOOT_USB_DEVICE,在<<读U-Boot源码-C语言编程技巧总结篇二>>对类似的宏有过推导,这里简要描述方便理解。

#define U_BOOT_USB_DEVICE(__name, __match) \
    ll_entry_declare(struct usb_driver_entry, __name, usb_driver_entry) = {\
        .driver = llsym(struct driver, __name, driver), \
        .match = __match, \
        }

#define ll_entry_declare(_type, _name, _list)                \
    _type _u_boot_list_2_##_list##_2_##_name __aligned(4)        \
            __attribute__((unused,                \
            section(".u_boot_list_2_"#_list"_2_"#_name)))    

#define llsym(_type, _name, _list) \
        ((_type *)&_u_boot_list_2_##_list##_2_##_name)

对U_BOOT_USB_DEVICE展开为:

ll_entry_declare(struct usb_driver_entry, mcs7830_eth, usb_driver_entry) = {\
    .driver = llsym(struct driver, mcs7830_eth, driver), \
    .match = mcs7830_eth_id_table, \
}

对ll_entry_declare以及llsym展开为:

struct usb_driver_entry _u_boot_list_2_usb_driver_entry_2_mcs7830_eth __aligned(4) \
    __attribute__((unused,    \
    section(".u_boot_list_2_""usb_driver_entry""_2_""mcs7830_eth")))={\
    .driver = ((struct driver *)&_u_boot_list_2_mcs7830_eth_2_mcs7830_eth), \
    .match = mcs7830_eth_id_table, \
}    

而usb_driver_entry为:

struct usb_driver_entry {
    struct driver *driver;
    const struct usb_device_id *match;
};

设备的加载在设备树框架中,通过读取设备树文件自动加载完成,这里不做展开。

本文出自微信公中号:嵌入式客栈,更过内容请关注
微信图片_20200301143209.jpg

推荐阅读
关注数
2832
内容数
162
分享一些在嵌入式应用开发方面的浅见,广交朋友
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息