UNDI全称Universal Network Driver Interface,它虽然属于UEFI网络框架的一部分,但是并没有在EDK开源代码中实现。不过目前主流网卡厂商都会提供UEFI下的网络驱动,并且大部分都实现了UNDI,这样BIOS下就可以通过SNP来调用网卡的底层驱动。本节不会具体说明网卡驱动的实现,而将重点放在UNDI框架以及它与SNP的关系。
UNDI在UEFI网络协议栈中的关系图:
UNDI说到底是UEFI规范中定义的一系列接口,然后SNP可以访问这些接口,达到网卡初始化和网络通信的目的。
SNP如何获取到这些接口呢?这需要实现了UNDI的网络设备驱动在初始化时安装一个Network Interface Identifier(NII)协议,目前它的版本是3_10:
///
/// An optional protocol that is used to describe details about the software
/// layer that is used to produce the Simple Network Protocol.
///
struct _EFI_NETWORK_INTERFACE_IDENTIFIER_PROTOCOL {
UINT64 Revision; ///< The revision of the EFI_NETWORK_INTERFACE_IDENTIFIER protocol.
UINT64 Id; ///< The address of the first byte of the identifying structure for this network
///< interface. This is only valid when the network interface is started
///< (see Start()). When the network interface is not started, this field is set to zero.
UINT64 ImageAddr; ///< The address of the first byte of the identifying structure for this
///< network interface. This is set to zero if there is no structure.
UINT32 ImageSize; ///< The size of unrelocated network interface image.
CHAR8 StringId[4]; ///< A four-character ASCII string that is sent in the class identifier field of
///< option 60 in DHCP. For a Type of EfiNetworkInterfaceUndi, this field is UNDI.
UINT8 Type; ///< Network interface type. This will be set to one of the values
///< in EFI_NETWORK_INTERFACE_TYPE.
UINT8 MajorVer; ///< Major version number.
UINT8 MinorVer; ///< Minor version number.
BOOLEAN Ipv6Supported; ///< TRUE if the network interface supports IPv6; otherwise FALSE.
UINT16 IfNum; ///< The network interface number that is being identified by this Network
///< Interface Identifier Protocol. This field must be less than or
///< equal to the (IFcnt | IFcntExt <<8 ) fields in the !PXE structure.
};
SNP就可以通过对应的GUID来访问到它,代码如下(位于NetworkPkg\SnpDxe\Snp.c):
//
// Get the NII interface.
//
Status = gBS->OpenProtocol (
Controller,
&gEfiNetworkInterfaceIdentifierProtocolGuid_31,
(VOID **)&Nii,
This->DriverBindingHandle,
Controller,
EFI_OPEN_PROTOCOL_BY_DRIVER
);
DEBUG ((DEBUG_INFO, "Start(): UNDI3.1 found\n"));
Pxe = (PXE_UNDI *)(UINTN)(Nii->Id);
注意代码最后的强转,得到了NII中最重要的结构体PXE_UNDI
。
关于NII的安装,以Intel的网卡源码GigUndiDxe\Init.c为例,可以找到如下的代码:
EFI_STATUS
InitNiiProtocol (
IN UNDI_PRIVATE_DATA *UndiPrivateData
)
{
NiiProtocol31 = &UndiPrivateData->NiiProtocol31;
NiiProtocol31->Id = (UINT64) (UINTN) mE1000Pxe31; // 这个就是PXE_UNDI
NiiProtocol31->IfNum = UndiPrivateData->IfId;
NiiProtocol31->Revision = EFI_NETWORK_INTERFACE_IDENTIFIER_PROTOCOL_REVISION_31;
NiiProtocol31->Type = EfiNetworkInterfaceUndi;
NiiProtocol31->MajorVer = PXE_ROMID_MAJORVER;
NiiProtocol31->MinorVer = PXE_ROMID_MINORVER_31;
NiiProtocol31->ImageSize = 0;
NiiProtocol31->ImageAddr = 0;
NiiProtocol31->Ipv6Supported = TRUE;
NiiProtocol31->StringId[0] = 'U';
NiiProtocol31->StringId[1] = 'N';
NiiProtocol31->StringId[2] = 'D';
NiiProtocol31->StringId[3] = 'I';
Status = gBS->InstallMultipleProtocolInterfaces (
&UndiPrivateData->DeviceHandle,
&gEfiNetworkInterfaceIdentifierProtocolGuid_31,
NiiProtocol31,
NULL
);
}
PXE_UNDI
结构体格式如下:
typedef union u_pxe_undi {
PXE_HW_UNDI hw;
PXE_SW_UNDI sw;
} PXE_UNDI;
可以看到它存在两种类型,从字面意思上看一种是硬件的格式,一种是软件的格式。对应的结构体描述如下(这种结构体有一个奇怪的名字叫!PXE
,不知道这里的叹号表示什么意思。由于BIOS下PXE主要用来形容一种网络启动方式,所以这里的!PXE
似乎是想说明这是一种“并不是用来启动的网络”?):
从图中可以看出来,硬件UNDI和软件UNDI有一个重要区别:即硬件UNDI通过往MMIO或者IO寄存器写命令(Command)来调用底层接口,而软件UNDI通过网络设备驱动提供出来的入口(Entry Point)来调用底层接口。UEFI下主要关注的是S/W的版本:
typedef struct s_pxe_sw_undi {
PXE_UINT32 Signature; ///< PXE_ROMID_SIGNATURE.
PXE_UINT8 Len; ///< sizeof(PXE_SW_UNDI).
PXE_UINT8 Fudge; ///< makes 8-bit cksum zero.
PXE_UINT8 Rev; ///< PXE_ROMID_REV.
PXE_UINT8 IFcnt; ///< physical connector count lower byte.
PXE_UINT8 MajorVer; ///< PXE_ROMID_MAJORVER.
PXE_UINT8 MinorVer; ///< PXE_ROMID_MINORVER.
PXE_UINT8 IFcntExt; ///< physical connector count upper byte.
PXE_UINT8 reserved1; ///< zero, not used.
PXE_UINT32 Implementation; ///< Implementation flags.
PXE_UINT64 EntryPoint; ///< API entry point.
PXE_UINT8 reserved2[3]; ///< zero, not used.
PXE_UINT8 BusCnt; ///< number of bustypes supported.
PXE_UINT32 BusType[1]; ///< list of supported bustypes.
} PXE_SW_UNDI;
从目前SNP的实现来看,硬件UNDI并不支持:
if ((Pxe->hw.Implementation & PXE_ROMID_IMP_HW_UNDI) != 0) {
Snp->IsSwUndi = FALSE;
Snp->IssueUndi32Command = &IssueHwUndiCommand;
} else {
Snp->IsSwUndi = TRUE;
if ((Pxe->sw.Implementation & PXE_ROMID_IMP_SW_VIRT_ADDR) != 0) {
Snp->IssueUndi32Command = (ISSUE_UNDI32_COMMAND) (UINTN) Pxe->sw.EntryPoint;
} else {
Snp->IssueUndi32Command = (ISSUE_UNDI32_COMMAND) (UINTN) ((UINT8) (UINTN) Pxe + Pxe->sw.EntryPoint);
}
}
因为这里的IssueHwUndiCommand()
并没有实现:
EFI_STATUS
EFIAPI
IssueHwUndiCommand (
UINT64 Cdb
)
{
DEBUG ((DEBUG_ERROR, "\nIssueHwUndiCommand() - This should not be called!"));
if (Cdb == 0) {
return EFI_INVALID_PARAMETER;
}
//
// %%TBD - For now, nothing is done.
//
return EFI_UNSUPPORTED;
}
所以目前BIOS下使用的都是软件UNDI,它的接口是从!PXE
这个结构体中获取的,所以最终在SNP模块中使用IssueUndi32Command()
函数相当于调用UNDI中的某个EntryPoint。
IssueUndi32Command()
函数的声明如下:
typedef
EFI_STATUS
(EFIAPI *ISSUE_UNDI32_COMMAND)(
UINT64 Cdb
);
它接受一个参数cdb
,虽然从这里看类型是UINT64
,但是其实它是一个指针,指向结构体PXE_CDB
,其描述如下:
对应到代码中的结构体是PXE_CDB
:
typedef struct s_pxe_cdb {
PXE_OPCODE OpCode;
PXE_OPFLAGS OpFlags;
PXE_UINT16 CPBsize;
PXE_UINT16 DBsize;
PXE_UINT64 CPBaddr;
PXE_UINT64 DBaddr;
PXE_STATCODE StatCode;
PXE_STATFLAGS StatFlags;
PXE_UINT16 IFnum;
PXE_CONTROL Control;
} PXE_CDB;
下面简单说明:
OpCode
是操作码,不同的操作码对OpFlags
、CPB结构体、DB结构体(就是CPBxxx,DBxxx那几个成员,它们对应到结构体中)都会有影响。StatCode
和StatFlags
是返回的参数,也受到OpCode
的影响。IFnum
用来处理一个NII对应多个物理网络设备的情况,值从0开始,算是一个Index。Control
可以指示使用了一个CDB还是多个,还可以指示当操作忙时是等待命令执行还是直接返回失败。OpCode
的值可以在UefiPxe.h中找到:
///
/// Return UNDI operational state.
///
#define PXE_OPCODE_GET_STATE 0x0000
///
/// Change UNDI operational state from Stopped to Started.
///
#define PXE_OPCODE_START 0x0001
///
/// Change UNDI operational state from Started to Stopped.
///
#define PXE_OPCODE_STOP 0x0002
///
/// Get UNDI initialization information.
///
#define PXE_OPCODE_GET_INIT_INFO 0x0003
///
/// Get NIC configuration information.
///
#define PXE_OPCODE_GET_CONFIG_INFO 0x0004
///
/// Changed UNDI operational state from Started to Initialized.
///
#define PXE_OPCODE_INITIALIZE 0x0005
///
/// Re-initialize the NIC H/W.
///
#define PXE_OPCODE_RESET 0x0006
///
/// Change the UNDI operational state from Initialized to Started.
///
#define PXE_OPCODE_SHUTDOWN 0x0007
///
/// Read & change state of external interrupt enables.
///
#define PXE_OPCODE_INTERRUPT_ENABLES 0x0008
///
/// Read & change state of packet receive filters.
///
#define PXE_OPCODE_RECEIVE_FILTERS 0x0009
///
/// Read & change station MAC address.
///
#define PXE_OPCODE_STATION_ADDRESS 0x000A
///
/// Read traffic statistics.
///
#define PXE_OPCODE_STATISTICS 0x000B
///
/// Convert multicast IP address to multicast MAC address.
///
#define PXE_OPCODE_MCAST_IP_TO_MAC 0x000C
///
/// Read or change non-volatile storage on the NIC.
///
#define PXE_OPCODE_NVDATA 0x000D
///
/// Get & clear interrupt status.
///
#define PXE_OPCODE_GET_STATUS 0x000E
///
/// Fill media header in packet for transmit.
///
#define PXE_OPCODE_FILL_HEADER 0x000F
///
/// Transmit packet(s).
///
#define PXE_OPCODE_TRANSMIT 0x0010
///
/// Receive packet.
///
#define PXE_OPCODE_RECEIVE 0x0011
///
/// Last valid PXE UNDI OpCode number.
///
#define PXE_OPCODE_LAST_VALID 0x0011
这里包含了所有需要传递从SNP传递给UNDI的信息索引,不过并不是所有网卡都支持这些操作。
然后简单说明SNP调用UNDI的流程,如下图所示:
从软件来看,实际上就是下面的几个步骤:
Snp->IssueUndi32Command()
,参数就是CDB。以SNP中的PxeStart()
函数为例:
/**
Call UNDI to start the interface and changes the snp state.
@param Snp pointer to snp driver structure.
@retval EFI_SUCCESS UNDI is started successfully.
@retval EFI_DEVICE_ERROR UNDI could not be started.
**/
EFI_STATUS
PxeStart (
IN SNP_DRIVER *Snp
)
{
PXE_CPB_START_31 *Cpb31;
Cpb31 = Snp->Cpb;
//
// Initialize UNDI Start CDB for H/W UNDI
//
Snp->Cdb.OpCode = PXE_OPCODE_START;
Snp->Cdb.OpFlags = PXE_OPFLAGS_NOT_USED;
Snp->Cdb.CPBsize = PXE_CPBSIZE_NOT_USED;
Snp->Cdb.DBsize = PXE_DBSIZE_NOT_USED;
Snp->Cdb.CPBaddr = PXE_CPBADDR_NOT_USED;
Snp->Cdb.DBaddr = PXE_DBADDR_NOT_USED;
Snp->Cdb.StatCode = PXE_STATCODE_INITIALIZE;
Snp->Cdb.StatFlags = PXE_STATFLAGS_INITIALIZE;
Snp->Cdb.IFnum = Snp->IfNum;
Snp->Cdb.Control = PXE_CONTROL_LAST_CDB_IN_LIST;
//
// Make changes to H/W UNDI Start CDB if this is
// a S/W UNDI.
//
if (Snp->IsSwUndi) {
Snp->Cdb.CPBsize = (UINT16)sizeof (PXE_CPB_START_31);
Snp->Cdb.CPBaddr = (UINT64)(UINTN)Cpb31;
Cpb31->Delay = (UINT64)(UINTN)&SnpUndi32CallbackDelay;
Cpb31->Block = (UINT64)(UINTN)&SnpUndi32CallbackBlock;
//
// Virtual == Physical. This can be set to zero.
//
Cpb31->Virt2Phys = (UINT64)(UINTN)0;
Cpb31->Mem_IO = (UINT64)(UINTN)&SnpUndi32CallbackMemio;
Cpb31->Map_Mem = (UINT64)(UINTN)&SnpUndi32CallbackMap;
Cpb31->UnMap_Mem = (UINT64)(UINTN)&SnpUndi32CallbackUnmap;
Cpb31->Sync_Mem = (UINT64)(UINTN)&SnpUndi32CallbackSync;
Cpb31->Unique_ID = (UINT64)(UINTN)Snp;
}
//
// Issue UNDI command and check result.
//
DEBUG ((DEBUG_NET, "\nsnp->undi.start() "));
(*Snp->IssueUndi32Command)((UINT64)(UINTN)&Snp->Cdb);
if (Snp->Cdb.StatCode != PXE_STATCODE_SUCCESS) {
//
// UNDI could not be started. Return UNDI error.
//
DEBUG (
(DEBUG_ERROR,
"\nsnp->undi.start() %xh:%xh\n",
Snp->Cdb.StatCode,
Snp->Cdb.StatFlags)
);
return EFI_DEVICE_ERROR;
}
//
// Set simple network state to Started and return success.
//
Snp->Mode.State = EfiSimpleNetworkStarted;
return EFI_SUCCESS;
}
SNP中的所有操作,实际上到最后都是使用类似上述的方式来完成的。最后简单说明CPB和DB,CPB的结构如下:
typedef struct s_pxe_cpb_start_31 {
///
/// PXE_VOID Delay(UINT64 UnqId, UINTN microseconds);
///
/// UNDI will never request a delay smaller than 10 microseconds
/// and will always request delays in increments of 10 microseconds.
/// The Delay() CallBack routine must delay between n and n + 10
/// microseconds before returning control to the UNDI.
///
/// This field cannot be set to zero.
///
UINT64 Delay;
///
/// PXE_VOID Block(UINT64 unq_id, UINT32 enable);
///
/// UNDI may need to block multi-threaded/multi-processor access to
/// critical code sections when programming or accessing the network
/// device. To this end, a blocking service is needed by the UNDI.
/// When UNDI needs a block, it will call Block() passing a non-zero
/// value. When UNDI no longer needs a block, it will call Block()
/// with a zero value. When called, if the Block() is already enabled,
/// do not return control to the UNDI until the previous Block() is
/// disabled.
///
/// This field cannot be set to zero.
///
UINT64 Block;
///
/// PXE_VOID Virt2Phys(UINT64 UnqId, UINT64 virtual, UINT64 physical_ptr);
///
/// UNDI will pass the virtual address of a buffer and the virtual
/// address of a 64-bit physical buffer. Convert the virtual address
/// to a physical address and write the result to the physical address
/// buffer. If virtual and physical addresses are the same, just
/// copy the virtual address to the physical address buffer.
///
/// This field can be set to zero if virtual and physical addresses
/// are equal.
///
UINT64 Virt2Phys;
///
/// PXE_VOID Mem_IO(UINT64 UnqId, UINT8 read_write, UINT8 len, UINT64 port,
/// UINT64 buf_addr);
///
/// UNDI will read or write the device io space using this call back
/// function. It passes the number of bytes as the len parameter and it
/// will be either 1,2,4 or 8.
///
/// This field can not be set to zero.
///
UINT64 Mem_IO;
///
/// PXE_VOID Map_Mem(UINT64 unq_id, UINT64 virtual_addr, UINT32 size,
/// UINT32 Direction, UINT64 mapped_addr);
///
/// UNDI will pass the virtual address of a buffer, direction of the data
/// flow from/to the mapped buffer (the constants are defined below)
/// and a place holder (pointer) for the mapped address.
/// This call will Map the given address to a physical DMA address and write
/// the result to the mapped_addr pointer. If there is no need to
/// map the given address to a lower address (i.e. the given address is
/// associated with a physical address that is already compatible to be
/// used with the DMA, it converts the given virtual address to it's
/// physical address and write that in the mapped address pointer.
///
/// This field can be set to zero if there is no mapping service available.
///
UINT64 Map_Mem;
///
/// PXE_VOID UnMap_Mem(UINT64 unq_id, UINT64 virtual_addr, UINT32 size,
/// UINT32 Direction, UINT64 mapped_addr);
///
/// UNDI will pass the virtual and mapped addresses of a buffer.
/// This call will un map the given address.
///
/// This field can be set to zero if there is no unmapping service available.
///
UINT64 UnMap_Mem;
///
/// PXE_VOID Sync_Mem(UINT64 unq_id, UINT64 virtual,
/// UINT32 size, UINT32 Direction, UINT64 mapped_addr);
///
/// UNDI will pass the virtual and mapped addresses of a buffer.
/// This call will synchronize the contents of both the virtual and mapped.
/// buffers for the given Direction.
///
/// This field can be set to zero if there is no service available.
///
UINT64 Sync_Mem;
///
/// protocol driver can provide anything for this Unique_ID, UNDI remembers
/// that as just a 64bit value associated to the interface specified by
/// the ifnum and gives it back as a parameter to all the call-back routines
/// when calling for that interface!
///
UINT64 Unique_ID;
} PXE_CPB_START_31;
虽然看到的都是UINT64的成员,但是它们其实都是一个个的函数指针,通过它UEFI可以给网卡驱动提供一些最基础的操作函数,比如延时操作,内存读写操作,IO读写操作等,这样的目的是为了能够处理不同的平台,上述基本操作的底层实现在不同的平台可能不同。
DB对应的结构体有很多,它表示的是UNDI的返回值,由于返回值不同,所以对应的结构体也会跟着改变,比如获取状态的结构体是这样的:
typedef struct s_pxe_db_get_status {
///
/// Length of next receive frame (header + data). If this is zero,
/// there is no next receive frame available.
///
PXE_UINT32 RxFrameLen;
///
/// Reserved, set to zero.
///
PXE_UINT32 reserved;
///
/// Addresses of transmitted buffers that need to be recycled.
///
PXE_UINT64 TxBuffer[MAX_XMIT_BUFFERS];
} PXE_DB_GET_STATUS;
获取MAC地址的结构体是这样的:
typedef struct s_pxe_db_mcast_ip_to_mac {
///
/// Multicast MAC address.
///
PXE_MAC_ADDR MAC;
} PXE_DB_MCAST_IP_TO_MAC;