PXE: Difference between revisions

From OSDev.wiki
Jump to navigation Jump to search
[unchecked revision][unchecked revision]
Content added Content deleted
(→‎Reading: Cleaned pseudocode.)
Line 144: Line 144:


<source lang="asm">
<source lang="asm">
; Note - due to comfort reasons, the author has decided to provide the following snippet in Intel (NASM) Assembly. If someone wishes to change it to a more generic format, he is welcome.
; Note - due to comfort reasons, the author has decided to provide the following snippet
; in Intel (NASM) Assembly. If someone wishes to change it to a more generic format, he is welcome.
t_PXENV_GET_CACHED:
t_PXENV_GET_CACHED:
.Status dw 0
.Status dw 0

Revision as of 20:50, 28 October 2013

Preboot eXecution Environment, or PXE, is the only way for Diskless Booting in PCs, and the standard is developed by Intel. It allows the Network Bootstrap Program (NBP) or the Bootloader to interface with the PXE, and load files via TFTP.

State of the Machine

Described by Section 4.4.5, Client State at Bootstrap Execution Time, the PXE is responsible for getting the boot code in the Client Machine. This is entirely similar to other boot devices, however, the BIOS Magic Signature, 0xAA55 isn't required, and the size of the boot file is limited to 32KiB, instead of the familiar 512 bytes.

Prior to Version 2.1 of the PXE specification, a PXENV+ Structure was given to the NBP. The PXE Entry Structure, or the !PXE structure, was introduced in Version 2.1, while the PXENV+ Structure has become obsolete. However, the address of the PXENV+ Structure is still given to the NBP during boot, to maintain compatibility with older NBPs.

The PXE Specification promises that the boot file is loaded at the address 0x0000:0x7C00, though it isn't trustable. Moreover,

  • ES:BX ES:BX should point to the PXENV+ structure, to maintain backward compatibility.
  • SS:SP SS:SP points to a valid stack, with at least 1.5KiB of free stack.
  • SS:SP + 4 SS:SP + 4 points to the newer !PXE structure, if and only the PXE Version is greater than or equal to 1.5.

PXENV+

The structure of the PXENV+ structure is as follows:

    // The Signature of the PXENV+ structure - contains "PXENV+".
    uint8_t  Signature[6];

    // The MSB contains the Major version number, while the LSB contains the minor version number.
    uint16_t Version;

    // The Length and the checksum of the structure, used for calculating the checksum.
    uint8_t  Length;
    uint8_t  Checksum;

    // A Far Pointer to the real mode PXE API entry point. 
    uint32_t RMEntry;

    // 32-bit offset to the protected mode API entry point. Instead of this, the !PXE structure is recommended.
    uint32_t PMOffset;

    // The rest of the fields don't matter much, and the Specifications can be referred.
    uint16_t PMSelector;
    uint16_t StackSeg;
    uint16_t StackSize;
    uint16_t BCCodeSeg;
    uint16_t BCCodeSize;
    uint16_t BCDataSeg;
    uint16_t BCDataSize;

    uint16_t UNDIDataSeg;
    uint16_t UNDIDataSize;
    uint16_6 UNDICodeSeg;
    uint16_t UNDICodeSize;

    // This is a far pointer to the "!PXE" structure, only present when the structure is present.
    uint32_t PXEPtr;

Recommended Usage

First of all, the integrity of the PXENV+ structure should be checked. This can be done by:

  • Checking the PXENV+ signature at the starting.
  • And, by checking the checksum of the structure.

If the structure fails to pass either of the above, boot should be aborted.

If the structure is good, the Version number should be checked. If it is higher than or equal to 0x0201 then the !PXE structure should be used. On the other hand, if it isn't, the PXENV+ structure should be used.

!PXE

The structure of the !PXE structure is as follows:

    // The Signature of the !PXE structure - contains "!PXE".
    uint8_t  Signature[4];

    // The Length and the checksum of the structure, used for calculating the checksum.
    uint8_t  Length;
    uint8_t  Checksum;
   
    // Contains zero - both of them.
    uint8_t Revision;
    uint8_t Reserved;

    // Far pointer to UNDI ROM ID structure and BC ROM ID structure.
    uint32_t UNDIROMID;
    uint32_t BCROMID;

    // A Far Pointer to the real mode PXE API entry point. 
    uint32_t RMEntry;

    // A Far Pointer to the protected mode API entry point. 
    uint32_t PMEntry;

    // The rest of the fields don't matter much (in the rest of the article), and the Specifications can be referred.

Recommended Usage

If the PXENV+ lists the revision as greater than or equal to 0x0201, then only must the !PXE stucture be relied upond. Then, the integrity of the !PXE structure should be checked. This can be done by:

  • Checking the !PXE signature at the starting.
  • And, by checking the checksum of the structure.

If the structure fails to pass either of the above, the PXENV+ structure should be used.

PXE API

The PXE Entry Point uses the Pascal calling convention - push the parameters on the stack, far call the entry point, clean up the stack - while the older PXENV+ Entry Point uses registers.

To access the API, a generic CallAPI function can be created, which eases out the calling. To do this, first of all, the address of the Entry Point, from either of the structures should be stored in a location (PXEAPI in the example). The PXE API accept the same parameters:

  • Opcode, or the API function number.
  • Segment:Offset address to a structure containing input data.

A example generic call function is as follows:

; Calls the PXE API, and abstracts things such that it works on both old and new APIs.
; @ds:di          The address of the input buffer.
; @bx             The opcode.
; NOTE: The above registers are what the legacy API also used, so it should cause no problem with it.
UsePXEAPI:
    ; And we push it over here, so that we have no problems with the new API.
    push ds
    push di
    push bx
    
    call far [PXEAPI]
    
    add sp, 6                         ; Clean up the stack.
    ret

The PXE API returns a status flag in AX and the beginning of the input buffer passed. It is recommended to test both of these, where a non zero value indicates failure.

Files, via TFTP

Getting the files via TFTP require the TFTP server's IP address. Along with that, you might want the client machine's IP address, the DHCP server's IP address, the client's MAC address, and perhaps several other things.

While this might seem daunting at first, PXE defines a neat way to do this.

Getting "Cached Information"

The PXE API's Get Cached Information function, with opcode 0x0071, requires a "t_PXENV_CACHED_INFO" passed to it, and returns a "bootph" structure. This "bootph" structure contains important information about the client and server machine, which is what is exactly wanted.

The "t_PXENV_CACHED_INFO" follows the following structure:

; Note - due to comfort reasons, the author has decided to provide the following snippet
; in Intel (NASM) Assembly. If someone wishes to change it to a more generic format, he is welcome.
t_PXENV_GET_CACHED:
    .Status       dw 0
    .PacketType   dw 2
    .BufferSize   dw 0
       .BufferOff dw 0 
       .BufferSeg dw 0
    .BufferLimit  dw 0
  • .Status. This should be zero, and returns the status after the function call.
  • .PacketType. This is the type of the packet, and 2 means "PXENV_PACKET_TYPE_DHCP_ACK", which is what we require. For further information, read the specifications.
  • .BufferSize. The number of bytes copied into the segment and offset field given.
  • .BufferOff/Seg. This gives the output buffer, to which to copy the bootph structure to. If you want, keep this to zero, and PXE would return the address of the Bootph structure in the BC data segment.

To do a call, put the address of the above structure in ES:DI, put the opcode, 0x0071, in BX, and call the generic call function. A example call to the above function is, as follows:

    mov di, t_PXENV_GET_CACHED
    mov bx, GET_CACHED_INFO           ; 0x0071.
    
    call UsePXEAPI                    ; Ask for the cached info.
    
    or ax, [t_PXENV_GET_CACHED]       ; Get the status into AX.
    test ax, ax
    jnz .Error

Bootph has a little complex structure, and the Specification should be referenced to know more about it. For now, the following is what you may require:

  • uint8_t CIP[4]. The Client IP address, at offset 12.
  • uint8_t YIP[4]. "Your" IP address, at offset 16.
  • uint8_t SIP[4]. Server IP address, at offset 20.
  • uint8_t GIP[4]. Relay agent IP address, at offset 24.
  • uint8_t MAC[16]. The MAC IP address, present at offset 28.
  • uint8_t SER[64]. The server host name, a asciiz string, at offset 40.
  • uint8_t BOO[128]. The Boot File name, a asciiz string, at offset 104.

Opening

PXE offers "TFTP Open", "TFTP Read" and "TFTP Close", which could be used like other common file I/O operations. It also offers a "TFTP Read File" - but it is recommended to use the former, which is more configurable.

PXENV_TFTP_OPEN, with opcode 0x0020 is required to open a file via TFTP. It takes a t_PXENV_TFTP_OPEN structure as a parameter, which has the following format:

t_PXENV_TFTP_OPEN:
    .Status       dw 0
    .SIP          dd 0
    .GIP          dd 0
    .Filename     times 128 db 0
    .Port         dw 0
    .PacketSize   dw 0
  • .Status. The status word.
  • .SIP. The Server IP, which we just obtained.
  • .Filename. A null terminated string, of the name of the file to open.
  • .Port. This should contain 69, or the UDP Port.
  • .PacketSize. This contains the size of each packet read. Sizes of 512, 1024 and something small are recommended. Usually, bigger sizes load the file faster, but aren't supported much.

NOTE: The SIP, GIP (0 means default) and Port are all big endian.

Reading

The PXENV_TFTP_READ function, with opcode 0x0022, reads a single packet from the currently opened file. The size of the packet is set in the Open call, while the function requires a t_PXENV_TFTP_READ structure.

The t_PXENV_TFTP_READ has the following structure:

t_PXENV_TFTP_READ:
    .Status       dw 0
    .PacketNumber dw 0
    .BufferSize   dw 0
    .BufferOff    dw 0
    .BufferSeg    dw 0
  • .Status. This contains the status returned after the function call.
  • .PacketNumber. This contains the packet number sent by the TFTP server. This is used to read packets in a order. Think of it like a index into the packets of the file - used to track of the current index.
  • .BufferSize. The buffer size - number of bytes written in the packet buffer. It is the last packet if this is less than the size of the packet negotiated.
  • .BufferOff/Seg. The address where you want to read the file packet.

What you want to do here is to keep reading packets from the file by increasing the '.BufferOff/Seg' address, and checking if the size of the packet read is less than the size negotiated in TFTP_OPEN. It can be concluded that several implementations use 0 to indicate EOF, while other implementations give a 'packet read' size less than the size negotiated.

With the above point in mind, the following pseudo code can be used to read a file:

while (bytes_left > 0)
{
    call_api();

    // AX register contains whether it failed or not, while Status contains the detailed status.
    if (status || asm("ax"))                  
        panic();

    if (bytes_read < PACKET_SIZE)
        break;

    bytes_left -= PACKET_SIZE;
}

Closing

The PXENV_TFTP_CLOSE, with opcode 0x0021, is perhaps the simplest function. It requires a input buffer, with the following format, to be passed to it:

t_PXENV_TFTP_CLOSE:
    .Status       dw 0
  • .Status. The Status returned after calling the function. The function would most probably ONLY fail if there is no active TFTP connection.

Cleaning up

After you have finished reading files via PXE, the best idea would be to shutdown PXE and reclaim any memory it was using. PXE support is unloaded by using the "PXEAPI_UNDI_SHUTDOWN" function to restore the network adapter to it's default state, followed by the "PXENV_UNLOAD_STACK" function to unhook any IRQ handlers.

Lastly, PXE needs to prepare itself to be removed from memory (unhook interrupt 0x1A) using the "PXENV_STOP_UNDI" function (for PXE) or the "PXENV_UNDI_CLEANUP" function (for PXENV+).

Once PXE is unloaded, memory can be reclaimed by checking the UNDI code segment and UNDI data segment start addresses and sizes (in the PXENV+ data structure), calculating "start address + size" and selecting the highest value. This value is the "new" number of bytes of RAM starting at 0x00000000 (or the amount of conventional memory), and can be used directly and/or the value at 0x40:0x13 (or 0x00000413) can be changed to reflect the new number of 1 KB blocks of conventional memory.

Please note that it's probably a good idea to calculate the amount of conventional memory before you unload PXE, because some of the functions above may clear the segment addresses and sizes in the PXENV+ structure.

See Also

Articles

External Links