Eigene Betriebssystementwicklung am PC  -  Teil 4

Stand 17.03.2013 - Dr. Erhard Henkes             

Teil 1    Teil 2    Teil 3   


Inhaltsübersicht

 

Entwickeln in der "Community"

Am Ende des dritten Teils wies ich auf das von mir ins Leben gerufene Projekt "OS-Development" hin. Der Sinn der "Community", die sich um das Entwickler-Team gebildet hat, und die wesentlichen Inhalte von PrettyOS, der Gegenstand des Projektes, sollen in diesem Teil näher beschrieben werden.

Was unterscheidet das Entwickeln in einer Gruppe von der Arbeit alleine? Zuallererst gibt es da die Meinungs- und Ideenvielfalt. Das ist sowohl bezüglich Tools, Kommunikationswege und Design des OS ein nicht zu unterschätzender Aufwand. Wir haben den Vorzug, bei c-plusplus ein Subforum für OS-Development betreiben zu dürfen. Dafür bin ich Marcus Bäckmann sehr dankbar. Zusätzlich legte ich einen chat-Kanal #PrettyOS an bei irc.euirc.net, damit Interessierte real-time kommunizieren zu können. Ebenfalls wichtig für die gemeinsame Arbeit am Sourcecode ist ein Repositorium. Wir entschieden uns für SVN auf sourceforge.net. Dort sind auch Bug- und Request-Tracker, die wir nutzen.

Ansonsten bietet sicj unter MS Windows das Projektmanagement  (zur Verwaltung der Files) des kostenfreien MSVC++ Express 2010 an. Die Projektfiles findet man im "Repo".
Forum, Chat, Repo, Projektverwaltung, damit kann man ganz brauchbar loslegen. Wir verzichteten auf ein eigenes Wiki, da es davon bereits genügend gibt, an denen man sich beteiligen kann.

PrettyOS

Das OS befindet sich in ständiger Entwicklung. Nachfolgend werden die wesentlichen Elemente dargestellt, die man zum Verständnis der Zusammenhänge und Abläufe in PrettyOS benötigt. Einen allgemeinen Überblick findet man hier.

Memory Management


Entwickler "Badestrand" hat unser Paging und Heap Modul komplett überarbeitet. Die Beschreibung findet man hier.

Die Aufteilung und das Management des Speichers ist eine wichtige Angelegenheit. Wir haben uns prinzipiell für folgenden Aufbau entschieden:

 0 MB - 16 MB        For DMA only. Kernel code resides somewhere in between
16 MB - 20 MB        For placement allocation while the heap is not set up.
                     Here resides:
                       - "Physical" bit table                        (128 KB)
                       - Kernel page directory                       (circa 8 KB)
                       - Kernel's page tables for 0 MB - 20 MB       (20 KB)
                       - Kernel's page tables for 3 GB - 4 GB        (1024 KB)
                       - Heap's "region" list (nongrowable)          (Remaining)
20 MB - 3 GB         Intended for user programs' code, stack, heap
 3 GB - 0xFFF00000   For the kernel heap only; malloc'd memory resides here
0xFFF00000 - 4 GB    Memory mapped stuff, e.g. for the PCI area

Der Kernel befindet sich momentan bei 0x100000 (früher bei 0x40000) und hat eine Größe, die man am besten aktuell aus kernel.map ablesen kann.
Dieser Datei-Typ "map" verschafft einen hervorragenden Überblick über das aus dem Sourcecode entstandene binäre Konstrukt. Wir verwenden es momentan für den Bootloader Stage 2, den Kernel und User-Programme.

Map-Dateien

Erstellt wird eine Map-Datei mittels Parameter -Map im makefile:

bootloader stage 2 (Assembler NASM):
[map symbols boot2.map]

kernel:
$(LD) $(LDFLAGS) $(addprefix $(OBJDIR)/,$(KERNEL_OBJECTS)) -T $(KERNELDIR)/kernel.ld -Map $(KERNELDIR)/kernel.map -o $(KERNELDIR)/KERNEL.BIN
user:
$(LD) *.o -T $(USERTOOLS)/user.ld -Map user.map $(LDFLAGS) -o HELLO.ELF

Beispielhaft zeige ich hier die Map-Datei des Kernels in gekürzter Form:

Memory Configuration

Name             Origin             Length             Attributes
*default*        0x00000000         0xffffffff

Linker script and memory map

LOAD object_files/kernel/kernel.o
...
LOAD object_files/kernel/process.o

                0x00100000                . = 0x100000
                0x00100000                __kernel_beg = .

.text           0x00100000    0x10fcb
                0x00100000                __code_start = .
 .text          0x00100000       0x1d object_files/kernel/kernel.o
 *fill*         0x0010001d        0x3 00
 .text          0x00100020      0x43c object_files/kernel/ckernel.o
                0x00100020                _showMemorySize
                0x00100076                _main
 .text          0x0010045c       0x8a object_files/kernel/cmos.o
                0x0010045c                _cmos_write
                0x001004a8                _cmos_read
 *fill*         0x001004e6        0x2 00
 .text          0x001004e8      0x821 object_files/kernel/console.o
                0x00100c37                _console_init
  ...

 .text          0x00110f90       0x3b object_files/kernel/process.o
                0x00110f90                _read_eip
                0x00110f93                _copy_page_physical
                0x00111000                . = ALIGN (0x1000)

.text1          0x00111000     0x3f1d
 object_files/kernel/data.o(.text)
 .text          0x00111000     0x3f1d object_files/kernel/data.o
                0x00111000                _file_data_start
                0x00114f1d                _file_data_end

.data           0x00114f20      0x8c6
                0x00114f20                __data_start = .
 *(.data)
 .data          0x00114f20        0x4 object_files/kernel/ckernel.o
                0x00114f20                _version
 .data          0x00114f24        0x0 object_files/kernel/cmos.o
 .data          0x00114f24        0x2 object_files/kernel/console.o
                0x00114f24                _displayedConsole
...
.data           0x00114fe0      0x806 object_files/kernel/interrupts.o

.rodata         0x00115800      0xb40
                0x00115800                __rodata_start = .
 *(.rodata)
 .rodata        0x00115800      0x154 object_files/kernel/console.o
 .rodata        0x00115954        0x4 object_files/kernel/ehciQHqTD.o
                0x00115954                _CSWMagicNotOK
...

 .rodata        0x00115ea0      0x158 object_files/kernel/util.o
                0x00115ff4                _INT_MAX
 .rodata        0x00115ff8      0x348 object_files/kernel/video.o
 *(.rdata)
 .rodata.str1.4  0x00116340     0x193b
 .rodata.str1.4
                0x00116340       0x7b object_files/kernel/ckernel.o
...

 .rodata.cst4   0x0011956c        0x4 object_files/kernel/time.o
 .rodata.cst4   0x00119570       0x1c object_files/kernel/util.o

.bss            0x001195a0     0x8f84
                0x001195a0                __bss_start = .
 *(.bss)
 .bss           0x001195a0        0x8 object_files/kernel/ckernel.o
                0x001195a0                _system
  ...

 .bss           0x00122520        0x4 object_files/kernel/cdi/cdi_storage.o
 *(COMMON)
                0x00122524                __start_cdi_drivers = .

.cdi            0x00122524        0x0
                0x00122524                __cdi_start = .
 *(.cdi)
                0x00122524                __stop_cdi_drivers = .

                0x00122524                __kernel_end = .
                0x00122524                __end = .
OUTPUT(kernel/KERNEL.BIN binary)

.comment        0x00000000      0x3a8
 .comment       0x00000000       0x12 object_files/kernel/ckernel.o
 .comment       0x00000012       0x12 object_files/kernel/cmos.o

...

 .comment       0x00000396       0x12 object_files/kernel/cdi/cdi_storage.o

 

... und zum Vergleich die gekürtzte Map-Datei eines User-Programms:

Memory Configuration

Name             Origin             Length             Attributes
*default*        0x00000000         0xffffffff

Linker script and memory map

LOAD hello.o
LOAD start.o
LOAD userlib.o
                0x01400000                . = 0x1400000

.text           0x01400000     0x115d
                0x01400000                __code_start = .
 *(.text*)
 .text          0x01400000       0xac hello.o
                0x01400000                _main
 *fill*         0x014000ac        0x4 00
 .text          0x014000b0        0xc start.o
                0x014000b0                _start
 .text          0x014000bc     0x10a1 userlib.o
                0x01400d9a                _tan

 ...
               
0x01400171                _beep

.data           0x01401160        0x0
                0x01401160                __data_start = .
 *(.data)
 .data          0x01401160        0x0 hello.o
 .data          0x01401160        0x0 userlib.o

.rodata         0x01401160      0x408
                0x01401160                __rodata_start = .
 *(.rodata)
 .rodata        0x01401160      0x408 userlib.o

.rodata.str1.4  0x01401568      0x24c
 .rodata.str1.4
                0x01401568      0x14d hello.o
 *fill*         0x014016b5        0x3 00
 .rodata.str1.4
                0x014016b8       0xfc userlib.o
                                 0xf9 (size before relaxing)

.rodata.str1.1  0x014017b4        0x4
 .rodata.str1.1
                0x014017b4        0x4 hello.o

.rodata.cst4    0x014017b8       0x20
 .rodata.cst4   0x014017b8       0x20 userlib.o

.rodata.cst8    0x014017d8        0x8
 .rodata.cst8   0x014017d8        0x8 userlib.o

.rel.dyn        0x014017e0        0x0
 .rel.text      0x00000000        0x0 hello.o

.bss            0x014017e0        0xc
                0x014017e0                __bss_start = .  
*(.bss)
 .bss           0x014017e0        0x0 hello.o
 .bss           0x014017e0        0xc userlib.o
 *(COMMON)
                0x014017ec                __end = .
OUTPUT(HELLO.ELF elf32-i386)

.comment        0x00000000       0x24
 .comment       0x00000000       0x12 hello.o
 .comment       0x00000012       0x12 userlib.o


... sowie die gekürtzte Map-Datei des Bootloaders Stage 2, die von NASM erzeugt wurde:

- NASM Map file ---------------------------------------------------------------

Source file:  stage2_bootloader/boot2.asm
Output file:  stage2_bootloader/BOOT2.BIN

-- Symbols --------------------------------------------------------------------

---- Section .text ------------------------------------------------------------

Real              Virtual           Name
             503               503  InstallGDT
             50D               50D  gdt_data
             50D               50D  NULL_Desc
             515               515  CODE_Desc
             51D               51D  DATA_Desc
             525               525  end_of_gdt
 ...             
8BC               8BC  print_string.done


Diese Map-Dateien sind für die Überprüfung des Aufbaus und für die Orientierung hilfreich, beispielsweise wenn man bei einem Debugger einen Haltepunkt einstellen will.
Auch zur Deutung von Adressen in Fehlermeldungen (z.B. #PF) sind diese Tabellen wichtig.

Die beiden Markierungen
0x00100000                __kernel_beg = .
0x00122524                __kernel_end = .

zeigen uns den Umfang des Kernels im Speicher. Er belegt dort 22524h = 140580 Byte

Die Shell wird übrigens hier "im Bauch des Kernels" in den Speicher und von dort in die zu erstellende RAMDisk transportiert:

 .text          0x00111000     0x3f1d object_files/kernel/data.o
                0x00111000                _file_data_start
                0x00114f1d                _file_data_end

Den Sourcecode findet man in data.asm:

; data for ramdisk
global _file_data_start
global _file_data_end

_file_data_start:

incbin "initrd.dat"
_file_data_end:

 

Heap

Der Heap verwendet sogenannte Regions, die eine Adresse, einen Reservierungsstatus und eine Größe besitzen. Zusätzlich Haben wir die regions Struktur noch mit einer laufenden Nummerierung und - das ist ausgefallen - mit einem string versehen, der die Verwendung beschreibt.
Damit kann man auf einfache Weise einen Heap-Logger erstellen, der jederzeit über die aktuelle detaillierte Verwendung des Heaps Auskunft erteilt.



Hier ein Beispiel für die Verwendung unserers ausgefallenen didaktischen mallocs mit einem String, der die Anwendung kurz erläutert:

floppy_t* fdd = malloc(sizeof(floppy_t), 0, "flpydsk-FDD");

Diese Strings kann man auch während der Laufzeit für die Diagnose von malloc und free verwenden, sodass man damit memory leaks kraftvoll begegnen kann.

Vom Bootloader zum User-Programm

Bootloader Stage 1 (BL1), der im Startsektor einer Partition residiert, wird vom BIOS per Boot-Signatur gefunden und gestartet. BL1 verwendet einen kleinen FAT12-Treiber, um BL2 namens boot2.bin im FAT12-Filesystem zu finden und zu laden. BL2 bewirkt den gleichen Vorgang, nur diesmal mit der Datei kernel.bin. Als Boot-Medium kann man eine Diskette oder ein usb Mass Storage Device (z.B. usb-stick) mit FAT12 Filesystem verwenden. Die Herstellung eine solchen usb-Mediums wird hier beschrieben.

BL2 kopiert zunächst im Real Mode den Kernel nach 0x3000. Dieser wird im Protected Mode nach 0x100000 umkopiert. Die Memory Maps (siehe GetMemoryMap.inc, INT 0x15, eax = 0xE820) werden im Real Mode an Speicherstelle 0x1000 übergeben. Dort holt sie der Kernel später ab.

paging.h:
#define MEMORY_MAP_ADDRESS 0x1000

paging.c, phys_init():
mem_map_entry_t* const entries = (mem_map_entry_t*)MEMORY_MAP_ADDRESS;


Wie oben gezeigt transportieren wir die "shell" (shell.elf, eingebunden in initrd.dat) via incbin in den kernel. Sie landet in der RAMDisk und wird von dort "geladen":

ckernel.c:
if (strcmp(node->name, "shell") == 0)
{
    shell_found = true;

    if (!elf_exec(buf, sz, "Shell"))
    printf("Cannot start shell!\n");
}
elf_exec ist eine Funktion, die die ELF-Datei parst und das Programm an den vorgesehen Speicherort (0x1400000) transportiert.
Der Speicherort für den Code-Start wird im Linker-Script user.ld eingestellt:

ENTRY(_start)
OUTPUT_FORMAT(elf32-i386)
OUTPUT_ARCH(i386)
SECTIONS
{
    . = 0x1400000;
    .text   : { __code_start = .;   *(.text*)         }
    .data   : { __data_start = .;   *(.data)          }   
    .rodata : { __rodata_start = .; *(.rodata)        }
    .bss    : { __bss_start = .;    *(.bss) *(COMMON) }
     __end = .;    
}


Der Aufbau der ELF-Datei ist hier beschrieben.

Nachdem die Shell geladen wurde, können wir durch Eingabe von Pfad-Programm-Kombinationen ein Programm von einem Medium laden und starten.
Bei der Pfadangabe muss man den Aufbau, der in PrettyOS Verwendung findet, kennen:

Available ports:                                                               
                                                                              
Type    Number  Name            Inserted disk                                 
----------------------------------------------------------------------         
FDD     A       Floppy Dev 1    PRETTYOS                                       
FDD     B       Floppy Dev 2                                                   
RAM     C       RAM             RAMDisk                                       
----------------------------------------------------------------------         
                                                                              
                                                                              
Attached disks:                                                               
                                                                              
Type    Number  Name            Part.   Serial                                 
----------------------------------------------------------------------         
Floppy  1       PRETTYOS        0       PRETTYOS                               
RAMdisk 3       RAMDisk         0       786436                                 
----------------------------------------------------------------------


PrettyOS unterscheidet zunächst Ports, Disks, Partition, Filesystem, File.

Beispiele:
Port ist der usb-Slot, Disk der usb-Stick. Darauf befindet sich eine FAT32-Partition mit den entsprechenden Files.
Port ist das Floppydisk-Laufwerk, Disk die eingelegte Diskette mit FAT12-Partition.
Port ist das RAM, Disk die RAMDisk mit Anfang und Ende (Partition) und mit eigenem Filesystem.

Laden wir z.B. ttt.elf von Diskette, so geben wir ein: A:\ttt.elf oder 1:\ttt.elf oder A:1:\ttt.elf, und das File wird von Diskette geladen. Ist der usb-Stick auf Platz drei der Disk-Liste, dann entsprechend 3:\ttt.elf.
Die Ports sind hierbei statisch, während die Disk-Reihenfolge von der Abfolge beim Einbinden abhängt.
 
Das User-Programm wird dann entsprechend mit dem elf-Parser bearbeitet analog zu shell.elf.

User-Programme greifen auf Funktionen im Kernel über sogenannte syscalls zu. Nehmen wir hier das konkrete Beispiel eines User-Programms mit setScrollField(...). In der Datei userlib.c findet sich das Durchreichen zum Syscall:

void setScrollField(uint8_t top, uint8_t bottom)
{
    __asm__ volatile("int $0x7F" : : "a"(21), "b"(top), "c"(bottom));
}

Wie geht dies genau weiter bis zur Funktion im Kernel?

In syscall.c findet sich die Definition: DEFN_SYSCALL2(setScrollField, 21, uint8_t, uint8_t)
Im Array static void* syscalls[] steht an der Stelle Nr. 21: &setScrollField Damit gelangen wir zu dieser Kernel-Funktion.

Solche syscalls kosten Zeit durch den Overhead beim Wechsel von Ring 3 zu Ring 0 und zurück. Abschottung im Protected Mode hat nun mal ihren Preis.
Daher versucht PrettyOS, die Zahl der syscalls zu beschränken und möglichst viele Funktionen in der userlib.h/c direkt umzusetzen.
Dies führt leider zu doppeltem Code sowohl in der Userlib als auch im Kernel (z.B. util.c).

EHCI und USB 2.0

Ein definiertes Ziel von PrettyOS war es, neben der Floppydisk auch USB Geräte wie z.B. usb-Sticks oder usb-Festplatten zu verwenden. Um diese Idee zu verwirklichen, musste zunächst ein EHCI-Treiber für das Highspeed-USB installiert werden. Anschließend konnte diese neue Funktionalität, die bisher auf root-ports begrenzt ist, für USB 2.0 bulk-Transfers verwendet werden. In Verbindung mit einem neuen FAT-Modul (FAT12/16/32) kann man damit Files von/auf usb-Mass Storage Devices (usb msd)  lesen/schreiben.

EHCI

EHCI steht für Enhanced Host Controller Interface. Die Spezifikation findet man hier.

EHCI stellt nur High Speed USB Funktionalität zur Verfügung.. Im Normalfall residiert daneben ein sogenannter "companion controller", entweder OHCI oder UHCI, der sich um die langsameren full speed und low speed USB Geräte kümmert. Deshalb findet man beim PCI scan beide Typen von Controllern, also UHCI und EHCI bzw. OHCI und EHCI. Ältere Systeme (vor dem Jahr 2000) bieten nur UHCI oder OHCI an und können nicht von unserem EHCI- und USB-Treiber profitieren. PrettyOS verfügt bisher noch nicht über einen UHCI oder OHCI Treiber. Wir steuern z.Z. auch nur EHCI-root-ports an. Highspeed-usb-sticks an Root-ports mit FAT-Formatierung können jedoch problemlos betrieben werden.

USB 2.0

Es gibt zu USB zwei hervorragende Übersichten bei osdev.org und bei lowlevel wie auch eine knappe Einführung, die hier zum ersten Studium empfohlen seien. Die ausführliche Spezifikation findet man hier. Wir verwenden in PrettyOS bisher nur Control-Transfers und Bulk-Transfers.

Netzwerk

Zu diesem Thema findet man gute Übersichten im Internet. Orientierung liefert das sogenannte OSI-Modell. OSI steht als Abkürzung für "Open System Interconnection" und bedeutet übersetzt etwa "Offenes System für Kommunikationsverbindungen". Will man in einem OS dieses Thema umsetzen, dann beginnt man ganz unten in der Schicht 1, der „Bitübertragungsschicht“ (Physical Layer). Hierzu gehören als Hardware Netzwerkkarten und Hubs. Für die Netzwerkkarte des realen oder emulierten PCs benötigt man einen „Treiber“. In PrettyOS umfasst der rtl8139-Treiber Funktionen zum Einrichten der Karte, zum Senden und zum Empfangen. Die Informationen laufen über einen Interrupt. Erreicht ein Paket die Netzwerkkarte, so meldet der Interrupt-Handler dieses, der Treiber entfernt die vier Bytes der Checksum (CRC) und gibt die Daten an das kartenunabhängige interne Netzwerkinterface des OS weiter. In PrettyOS ist dies bei rtl8139 z.B.:

// Inform network interface about the packet
network_receivedPacket
(device->device, &device->RxBuffer[device->RxBufferPointer]+4, length - 4); // Strip CRC from packet.

Bei pcnet wird die CRC und evt. notwendiges Padding, damit das Ethernet-Paket mindestens 64 byte (incl. CRC) groß ist, bereits von der Karte entfernt. Daher muss die diesbezügliche Behandlung im Netzwerkkartentreiber erfolgen.

Die Weitergabe erfolgt nun an Schicht 2, die „Sicherungsschicht“ (Link Layer). Hierzu gehört das Ethernet-Protokoll. Als Hardware gehören Bridges und Switches zu dieser OSI-Schicht. Das zentrale Element ist hier die MAC-Nummer. Die ersten drei der insgesamt sechs Byte stehen hierbei für einen Hersteller der Netzwerk-Hardware. Zunächst geben wir die Daten an das Ethernet-Modul weiter.

EthernetRecv(buffer->adapter, (ethernet_t*)buffer->data, buffer->length);

Nun wird Protokoll für Protokoll nach oben hin „abgeschält“. Das Aufbauprinzip ist immer gleich. Vorne kommt ein definierter Header, dahinter die Daten. Die Daten des einen Protokolls enthält den Header des nächsten protokolls usw. Bei uns kommen folgende bei Ethernet Daten im OS an:

Dest MAC (6 byte) – Source MAC (6 byte) – Type/Length  (2 byte) – Data  (… byte) – (CRC wurde bereits abgeschnitten)

Der Header ist also 14 byte groß. Die zugehörige struct sieht wie folgt aus:

typedef struct
{
    uint8_t recv_mac[6];
    uint8_t send_mac[6];
    uint8_t type_len[2]; // Type(Ethernet 2) or Length(Ethernet 1) } __attribute__((packed)) ethernet_t;

So zieht sich das über die die Vermittlungs- und Transportschicht bis zur Anwendung durch.

|------------|-----------|------------|------------
| MAC-Header | IP-Header | TCP-Header | Daten                   
|------------|-----------|------------|------------

Die Daten des unteren Protokolls enthalten als Erstes den Header des darüber liegenden Protokolls.
Hier folgt zunächst der IP-Header:

typedef struct
{
    uint8_t  ipHeaderLength   : 4;
    uint8_t  version          : 4;
    uint8_t  typeOfService;
    uint16_t length;
    uint16_t identification;
    uint16_t fragmentation;
    uint8_t  ttl;
    uint8_t  protocol;
    uint16_t checksum;
    IP_t     sourceIP;
    IP_t     destIP;
} __attribute__((packed)) ipv4Packet_t;

Geht man von unten nach oben, so enthält der Header des unteren Protokolls einen Hinweis auf den nächsten zu erwartenden Header im Data-Bereich. Das ist notwendig, denn parallel zu jedem Protokoll gibt es viele andere Möglichkeiten, z.B. neben IP auch ARP, neben TCP auch UDP, usw.

Im Ethernet-Header (MAC_Header) bedeutet das Typfeld: 08 00 IPv4, 08 06 ARP, 86 DD IPv6, …

So wiederholt sich das im IP-Header mit dem Feld protocol. Hier die Verteilung in das richtige Protokoll-Modul:

switch(packet->protocol)
{
    case 1: // icmp         ICMPAnswerPing(adapter, (void*)packet+ipHeaderLengthBytes, ntohs(packet->length), packet->sourceIP);
        break;
    case 6: // tcp
        tcp_receive(adapter, (void*)packet+ipHeaderLengthBytes, packet->sourceIP, ntohs(packet->length)-ipHeaderLengthBytes);
        break;
    case 17: // udp
        UDPRecv(adapter, (void*)packet+ipHeaderLengthBytes, ntohs(packet->length)-ipHeaderLengthBytes);
        break;

htons/htonl und ntohs/ntohl sind wichtige makros, die die Portabilität des codes gewährleisten, denn sie sind unabhängig von Big und Little Endian.

#define htons(v) ((((v) >>  8) & 0xFF) | (((v) & 0xFF) << 8))
#define htonl(v) ((((v) >> 24) & 0xFF) | (((v) >> 8) & 0xFF00) | (((v) & 0xFF00) << 8) | (((v) & 0xFF) << 24))


Sicher wundert man sich über die folgende Definition:

#define ntohs(v) htons(v)
#define ntohl(v) htonl(v)

Also warum nicht gleich so etwas wie swap16 und swap32? Ganz einfach: der swap ist nicht das Entscheidende, sondern nur der Übergang von Netz zu Host und umgekehrt. Ob hierbei eine Vertauschung der Bytes notwendig ist, spielt für das Modul keine Rolle. Der Code selbst dient hierbei als "Dokumentation" der Richtung. Wichtig ist, dass man sofort nach der Entnahme aus Netzpaketen die Daten in das Host-Format wandelt, damit keine falschen Berechnungen stattfinden. Bei der Übergabe an ein Netzpaket wandelt man direkt vorher zurück von Host- nach Netz-Format.

Nun folgt der für das Internet so wichtige TCP-Header:

typedef struct
{
    uint16_t sourcePort;
    uint16_t destPort;
    uint32_t sequenceNumber;
    uint32_t acknowledgmentNumber;
    uint8_t  reserved   : 4;
    uint8_t  dataOffset : 4;                   // The number of 32 bit words in the TCP Header    
    // Flags (6 Bit)     uint8_t FIN : 1;                           // No more data from sender
    uint8_t SYN : 1;                           // Synchronize sequence numbers
    uint8_t RST : 1;                           // Reset     uint8_t PSH : 1;                           // Push     uint8_t ACK : 1;                           // Acknowledgment
    uint8_t URG : 1;                           // Urgent     // Flags only shown in wireshark
    uint8_t ECN : 1;                           // ECN (reserved)
    uint8_t CWR : 1;                           // CWR (reserved)
 
    
uint16_t window;
    uint16_t checksum;
    uint16_t urgentPointer;
} __attribute__((packed)) tcpPacket_t;

Die detaillierten Abläufe beim Datenaustausch via TCP-Protokoll findet man hier:
http://www.medianet.kent.edu/techreports/TR2005-07-22-tcp-EFSM.pdf

Im Gegensatz zu UDP ist TCP ein verbindungsorientiertes Protokoll. Man beginnt mit einem Dreiwege-Handshake: SYN – SYN,ACK – ACK
Hierbei unterscheidet man zwischen active und passive open, je nachdem, wer zuerst das SYN sendet. Ein Browser muss z.B. active open durchführen, also eine richtige connection mit zwei sockets anlegen und das SYN abschicken. Ein telnet server verwendet die Methode passive open, legt also nur einen socket an und geht auf LISTEN, um das SYN von einem fremden socket zu empfangen. Ein socket ist hierbei die Kombination aus IP-Adresse und Port-Nummer.

Anschließend erfolgt der Datenaustausch (Status: ESTABLISHED) über eine jeweilige Erhöhung der Sequenznummern und die Bestätigung mit ACK. Bei allen Paketen ist das Flag auf ACK gesetzt.

Beendet wird die Sitzung durch FIN oder RST.

Das Feld window dient der Steuerung der Paketgröße.

Da es Optionen gibt im TCP-Header, muss ein Feld data offset existieren, damit man weiß, wo die Daten beginnen. Daten können z.B. http, irc oder telnet sein. Das prägt dann die Anwendung.

Anwendung             (http, irc, telnet, …)               Anwendung

Transportschicht      (TCP)                                Transportschicht

Vermittlungsschicht   (IP)                                 Vermittlungsschicht

Sicherungsschicht     (MAC)                                Sicherungsschicht

Netzwerkarte           <------ Kabel, Treiber ------>      Netzwerkarte


Fortsetzung folgt

zurück zu:    Teil 1    Teil 2    Teil 3