;Copyright (c) 2009, 2010 Max Reitz
;
;Permission is hereby granted,  free of charge,  to any  person obtaining a
;copy of this software and associated documentation files (the "Software"),
;to deal in the Software without restriction,  including without limitation
;the rights to use,  copy, modify, merge, publish,  distribute, sublicense,
;and/or sell copies  of the  Software,  and to permit  persons to whom  the
;Software is furnished to do so, subject to the following conditions:
;
;The above copyright notice and this permission notice shall be included in
;all copies or substantial portions of the Software.
;
;THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
;IMPLIED, INCLUDING  BUT NOT  LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
;FITNESS FOR A PARTICULAR  PURPOSE AND  NONINFRINGEMENT.  IN NO EVENT SHALL
;THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
;LIABILITY,  WHETHER IN AN ACTION OF CONTRACT,  TORT OR OTHERWISE,  ARISING
;FROM,  OUT OF  OR IN CONNECTION  WITH THE  SOFTWARE  OR THE  USE OR  OTHER
;DEALINGS IN THE SOFTWARE.

;These variables help you to minimize the size of this executable. The default
;level is of course optimized a lot, but it's nice to look at and so on. By
;removing code the output gets "uglier", but still working.

;You may choose one of the following levels by setting Os_level to the
;corresponding value

;No optimization (0): Everything included

;First optimization level: Use it to suppress strings such as the signifant or
;the IP prompt. The newline at the end of the output is also removed.

;Second optimization level: You won't see your own MAC address and the MAC
;address to be searched won't contain colons as seperators (it simply will be a
;huge number, i. e. 52550A000202 instead of 52:55:0A:00:02:02). Exclamation
;marks in case of an error will be suppressed.

;Third optimization level: You may not input a decimal IP but a hexadecimal.
;It consists out of eight hexadecimal digits (each between '0' and '9' or 'a'
;and 'f' (NOT between 'A' and 'F'!)). You mustn't press enter when you've
;finished, a line break will be issued when eight digits have been input
;telling you the IP has been accepted.
;An example for such an IP would be "0a00022a" which is equal to 10.0.2.42

;Fourth optimization level: The IP is hardcoded, you are unable to input it
;"on-the-fly". Furthermore, the check for PCI BIOS extensions being available
;is disabled because it should be available everywhere today.

Os_level = 0

if Os_level >= 4
    ;You have to set the IPs here if the optimization level is beyond 3. Watch
    ;out: These values are in big endian, so the last byte from the IP is the
    ;most signifant byte here.
    ;10.0.2.15 - good for qemu
    my_ip   = 0x0F02000A
    ;10.0.2.2 - also good for qemu
    dest_ip = 0x0202000A
end if


use16
org 0x7C00

;Don't know if this is really necessary but it seems to be
cli
;Standard direction flag
cld

;Those have to be zero
xor   ax,ax
mov   ds,ax
mov   es,ax
mov   ss,ax
;Also CS
jmp far 0x0000:entry_point


if Os_level < 3
    ;Reads an IP
    ;SI: Prompt
    ;Returns in EAX: IP (big endian)
    read_ip:
    if Os_level < 1
        ;Print the prompt
        call  print
    end if
    ;An IP has 4 blocks
    mov   cx,4
    ;Every block is stored in bx
    xor   bx,bx
    ;EDX holds the IP
    xor   edx,edx
    read_ip_loop:
    ;Storing BX while we read the next character (will be destroyed by printc)
    push  bx
    ;Read a character
    xor   ax,ax
    int   0x16
    ;Print it
    call  printc
    pop   bx

    ;A dot is the seperator between blocks
    cmp   al,'.'
    je    ip_block_done
    ;Return means, the IP is finished (originally that value is CR == 13, but
    ;printc changes it to LF == 10)
    cmp   al,10
    jz    ip_block_done
    ;Getting the value from its ASCII character
    sub   al,'0'
    ;Every other character being no digit is not allowed
    jb    any_error
    cmp   al,9
    ja    any_error

    ;We'll add AX later, so we need to zero AH
    xor   ah,ah
    ;Multiply current block value by 5 (we don't care about the higher word)
    lea   ebx,[ebx*4+ebx]
    ;Multiply by 2 -> all in all 10
    shl   bx,1
    ;Add current digit
    add   bx,ax
    ;Block value must fit into one byte
    test  bh,bh
    jnz   any_error
    ;Read the next character
    jmp   read_ip_loop

    ip_block_done:
    ;One block is done
    ;Shift IP left one byte
    shl   edx,8
    ;Set this byte
    mov   dl,bl
    ;One block done, so decrement the block counter and check if it's zero
    loop  not_the_end_of_ip
    ;It's zero, so the user must have pressed return
    cmp   al,10
    jne   any_error
    ;Turn IP into big endian
    bswap edx
    ;And return
    ret

    not_the_end_of_ip:
    ;Zero the block value so we can read the next block
    xor   bx,bx
    ;IP not yet finished, so the user must have input a dot
    cmp   al,'.'
    ;If yes, read the next block
    je    read_ip_loop
else if Os_level < 4
    ;Reads a hexadecimal IP (small letters necessary)
    ;Returns in EAX: IP (big endian)
    read_ip:
    ;Zero EDX, it'll hold the IP
    xor   edx,edx
    ;4 bytes => 8 nibbles
    mov   cx,8
    read_ip_nibble:
    ;We want to read a character
    xor   ax,ax
    int   0x16
    ;Print it
    call  printc
    ;Get its value
    sub   al,'0'
    ;If it was below '0' the character was invalid
    jb    any_error
    ;If it is between 0 and 9 everything is OK
    cmp   al,9
    jna   read_ip_got_val
    ;If not we subtract 'a' so we get values between 0xA and 0xF
    sub   al,'a'-('0'+10)
    ;If it was below 'a' (but above '9'), the character was invalid
    jb    any_error
    ;The same thing also applies to characters above 'f'
    cmp   al,0xF
    ja    any_error
    read_ip_got_val:
    ;Set this nibble
    shl   edx,4
    or    dl,al
    ;And continue, if we read not all nibbles
    loop  read_ip_nibble
    ;Turn IP into big endian
    bswap edx
    ;And go to the next line
    mov   al,13
    call  printc
    ret
end if


;Any error happened
any_error:
if Os_level < 2
    ;Print an exclamation mark to indicate that
    mov   al,'!'
    call  printc
end if
hang:
;And hang
cli
hlt


entry_point:

;Activate interrupts (keyboard input)
sti

;Stack right below our program
mov   sp,0x7C00

if Os_level < 1
    ;Print the signifant - it's a "famous" anymal, but I think it's only known
    ;to members of the German "lowlevel" community ;-)
    ;You may visit the translation of a page explaining the signifant under:
    ;http://translate.google.com/translate?u=http://lowlevel.brainsware.org/wiki/index.php%3Ftitle%3DSignifant&sl=de&tl=en
    mov   si,signifant
    call  print
end if

if Os_level < 4
    ;Read two IPs (my IP first, then destination's IP)
    if Os_level < 1
        mov   si,ask_for_my_ip
    end if
    call  read_ip
    mov   [my_ip],edx
    call  read_ip
    mov   [dest_ip],edx
end if

if Os_level < 4
    ;Test for PCI BIOS
    mov   ax,0xB101
    int   0x1A
    ;Non-zero, if there's no PCI BIOS
    test  al,al
    jz    any_error
    ;edx == 'PCI ', if PCI BIOS available
    cmp   edx,'PCI '
    jne   any_error
end if
;Find a PCI device
mov   ax,0xB102
;Vendor ID: 0x10EC (Realtek)
mov   dx,0x10EC
;Device ID: 0x8139 (RTL 8139 network card)
mov   cx,0x8139
;Get the first device available
xor   si,si
int   0x1A
;Uh, error occured: No card found
jc    any_error
;Store the address (bus, device, function)
mov   [pciaddr],bx
;BAR0
mov   di,0x10
;Scan all BARs for an I/O space
bar_loop:
;Restore device address
mov   bx,[pciaddr]
;Read one dword from PCI configuration space
mov   ax,0xB10A
int   0x1A
;Uhm, f*ck
jc    any_error
;Is this an I/O space?
test  cl,1
;Yes, it is
jnz   bar_found
;Read the next BAR
add   di,0x04
;Have we already read all BARs?
cmp   di,0x28
;No, so check the next
jb    bar_loop
;Yes, we did. Hm.
jmp   any_error


bar_found:
;Hooray, we have our I/O space
;Getting the relevant bits
and   cx,0xFFF8
mov   [addr],cx

;Get the MAC address of our card
;Set DX to the beginning of the I/O space, its first six bytes represent the
;MAC
mov   dx,cx
;Read six bytes
mov   cx,6
;Store the value six (we'll need it later again, saves some bytes)
push  cx
;Store the MAC in my_mac
mov   di,my_mac
if Os_level < 2
    read_mac:
    ;Get a byte from the current port
    in    al,dx
    ;Next port, next byte of the MAC
    inc   dx
    ;Store byte of the MAC in my_mac
    stosb
    ;Print the value
    call  print_hexb
    ;One byte done
    dec   cx
    ;All bytes done?
    jcxz  mac_done
    ;No, so go on with printing a colon (seperating two bytes of the MAC)
    mov   al,':'
    call  printc
    ;Read the next byte
    jmp   read_mac
    mac_done:
    ;OK, our MAC has been printed, so go to the next line now
    mov   al,13
    call  printc
else
    read_mac:
    ;Get a byte from the current port and store it in [ES:DI]
    insb
    ;Next port, next byte of the MAC
    inc   dx
    ;One byte done, do next, if necessary
    loop  read_mac
end if

;Go to the COMMAND register
add   dx,0x31
;Reset the card
mov   al,0x10
out   dx,al

;Wait until reset done
wait_for_reset_done:
in    al,dx
;Still resetting?
test  al,0x10
jnz   wait_for_reset_done

;No, so activate reception and transmission now
mov   al,0x0C
out   dx,al

;wait a bit
call  wait_a_bit

;Go to TRANSMISSION CONFIG register
add   dx,0x09
;Read it
in    eax,dx
;Take those bits from it (they should not be changed right now)
and   eax,0x7FC00000
;Do 2 kB DMA transfers
or     ax,    0x0700
out   dx,eax

;Go to RECEIVE CONFIG register
add   dx,0x04
;Unlimited DMA, broadcast + multicast + singlecast (no promiscuous)
mov   eax,0x0000070D
out   dx,eax

;Go to RECEIVE BUFFER register
sub   dx,0x14
;Enter the address of our receive buffer here
mov   eax,receive_buf
out   dx,eax

;Go to INTERRUPT MASK register
add   dx,0x0C
;Disable all interrupts
xor   ax,ax
out   dx,ax

call  wait_a_bit

;Destination: everybody (broadcast)
mov   di,arp_eth_destMAC
;AL is still 0xFF
;Get the value six into CX - do you still remember that? ;-)
pop   cx
push  cx
;And store FF:FF:FF:FF:FF:FF (ethernet broadcast) into the "destination MAC"
;field
rep   stosb
;Source: me
;So get my MAC
mov   si,my_mac
;(Six bytes again)
pop   cx
push  cx
;And store it into the "source MAC" field
rep   movsb
;Packet type is ARP, of course (0x0806, but in Big Endian)
mov   ax,0x0608
stosw
;Now the ARP packet
;We use MAC as hardware address (0x0001 in Big Endian)
mov   ax,0x0100
;We'll need that value later again
push  ax
stosw
;IPv4 as protocol address (0x0800 in Big Endian)
mov   ax,0x0008 ;IPv4
stosw
;Every MAC address uses 6 bytes
mov   al,6
stosb
;And every IPv4 address uses 4 bytes
mov   al,4
stosb
;Doing an ARP request (code is 1, but in Big Endian, so 0x0100)
pop   ax
stosw
;My MAC address again
mov   si,my_mac
;6 bytes again
pop   cx
push  cx
;And store it into the ARP packet's "source MAC" field
rep   movsb
;My IP address (a dword)
if Os_level < 4
    mov   eax,[my_ip]
else
    mov   eax,my_ip
end if
;Store it into the "source IP" field
stosd
;Destination's MAC address should be here, but since it's a broadcast nobody
;cares
add   di,6
;Destination's IP address
if Os_level < 4
    mov   eax,[dest_ip]
else
    mov   eax,dest_ip
end if
;Store it into the "destination" IP field
stosd

;We will send only one packet - that makes everything a lot
;easier

;Go to TRANSMIT ADDRESS register
sub   dx,0x1C
xor   eax,eax
;Enter the address of the our packet
mov   ax,arp_packet
;address of our buffer
out   dx,eax
;Go to TRANSMIT STATUS register
sub   dx,0x10
;Packet length: It's equal to the minimum packet length (our ARP packet has a
;length of 42 bytes)
mov   ax,60
;Start transfer by setting the packet length
out   dx,eax

;Go to COMMAND register
add   dx,0x27
wait_for_packet:
;Packet arrived?
in    al,dx
test  al,1
;If no, continue to wait
jnz   wait_for_packet

;Woohoo, we have the packet, so print out the MAC it came from
mov   si,rec_arp_eth_srcMAC

;Yep, six bytes again ;-)
pop  cx
get_dest_mac:
;Load a byte
lodsb
;And print it
call  print_hexb
;One byte done - all bytes done?
loop  dmac_go_on
;Yes, so go the the next line and freeze afterwards
if Os_level < 1
    mov   al,13
    call  printc
end if
cli
hlt
dmac_go_on:
;No, so print a colon
if Os_level < 2
    mov   al,':'
    call  printc
end if
;And continue
jmp   get_dest_mac


;Print a character
;AL: ASCII code
printc:
;Print character
mov   ah,0x0E
;Attribute byte: 0x07; Page: 0x00
mov   bx,0x07
;And print it
int   0x10
cmp   al,13
jne   done_c
mov   al,10
call  printc
done_c:
ret

;Print one hex character (nibble)
;Lower 4 bits of AL: Value
do_one_hex_char:
;Add '0' first
add   al,'0'
cmp   al,'9'
jbe   may_print_hex_now
;If the value was bigger than 9, we have to add 'A'-10 instead of '0', so
;subtract '0' again
add   al,'A'-('0'+10)
may_print_hex_now:
;And print the resulting value
call  printc
ret

;Print a byte as hexadecimal value
;AL: Byte to be printed
print_hexb:
push  ax
;Higher nibble first
shr   al,4
call  do_one_hex_char
pop   ax
;Now the lower nibble
and   al,0xF
call  do_one_hex_char
ret

if Os_level < 1
    ;Prints a string
    ;SI: Pointer to a zero-terminated string
    print:
    ;Load a character
    lodsb
    ;If it's zero, we're done
    test  al,al
    jz    print_done
    ;No, so print it
    call  printc
    ;And continue
    jmp   print
    print_done:
    ;Done, so return
    ret
end if

;Waits a bit
wait_a_bit:
pusha
;Wait 66 ms
;86h: Wait some microseconds
mov   ah,0x86
;We cannot use 'cwd' here, because the MSB of AX is set
xor   dx,dx
xor   cx,cx
inc   cx
;These are 65.536 ms - should be enough
int   0x15
popa
ret

receive_buf  = 0x8000
rec_arp_eth_srcMAC  = 0x800A
arp_packet          = 0x9000
arp_eth_destMAC     = 0x9000 ;times 6 db
arp_eth_srcMAC      = 0x9006 ;times 6 db
arp_eth_packet_type = 0x900C ;dw
arp_pck_hwaddr_type = 0x900E ;dw
arp_pck_ptaddr_type = 0x9010 ;dw
arp_pck_hwaddr_size = 0x9012 ;db
arp_pck_ptaddr_size = 0x9013 ;db
arp_pck_operation   = 0x9014 ;dw
arp_pck_srcMAC      = 0x9016 ;times 6 db
arp_pck_srcIP       = 0x901C ;dd
arp_pck_destMAC     = 0x9020 ;times 6 db
arp_pck_destIP      = 0x9026 ;dd
my_mac  = 0xA000 ;times 6 db
addr    = 0xA006 ;dw
pciaddr = 0xA008 ;dw
if Os_level < 4
    my_ip   = 0xA00C ;dd
    dest_ip = 0xA010 ;dd
end if
tipbuf  = 0xA800

if Os_level < 1
    signifant:
    db '/\_/\',13
    db '\_',0xEC,' /',13
    db " '\|'",13,0

    ask_for_my_ip db "Me? ",0
    ask_for_d_ip db "Destination? ",0
end if

times 510-($-$$) db 0
dw 0xAA55
