TI2-Tutorium, 15.05.2013

Im Tutorium haben wir uns hauptsächlich mit IEEE-Fließkommazahlen beschäftigt: Umwandeln, Rechnen und Runden.

Zum 5. Übungszettel

Im folgenden ein paar Hinweise zu der letzten Aufgabe auf dem 5. Übungszettel.

C-Framework

Ihr könnt für die Aufgabe folgendes C-Framework benutzen:

// Funktionen für Standard-Ein-/Ausgabe einbinden
#include <stdio.h>

// Zahlentypen importieren, die fest definierte Bit-Längen haben
// Wir können damit "int" ersetzen, was auch je nach System nicht immer genau
// 32 bit sein muss…
// Eine Ganzzahl mit garantiert 32bit wäre int32_t, ohne Vorzeichen uint32_t
#include <inttypes.h>

// Funktionen um Strings zu manipulieren (wir benötigen die Funktion memset)
#include <string.h>

// Dies ist die Funktion, die eine 64bit (double precision) IEEE-Zahl entgegen-
// nimmt und einen Integer zurückgibt (natürlich ebenfalls 64bit)
uint64_t float_to_int(double);

// Diese Funktion müsst ihr implementieren
// Sie bekommt die IEEE-Zahl, einen Zeiger auf ein in C angelegten Speicher-
// bereich, in den der String als Bit-Darstellung reingeschrieben werden soll
// Zurückgegeben wird nichts
// Wir garantieren, dass der Speicherbereich >= 64 Zeichen ist, wir also ohne
// Probleme das Bitmuster reinschreiben können
void float_to_string(double, char[]);

// Konstanten definieren
#define SPEICHERGROESSE 65

int main() {
    // Unsere Kommazahl, die dargestellt werden soll
    double kommazahl = 345.2143;

    // Wird die Ganzzahl enthalten
    uint64_t ganzzahl;
    // Lege Speicher für Bit-String an
    char bitdarstellung[SPEICHERGROESSE];
    // Ein String in C endet, wenn das Zeichen mit ASCII-Code 0 gelesen wurde
    // Daher überschreiben wir erstmal alles, was zufällig in dem Speicher
    // liegt mit Nullen
    memset(bitdarstellung, 0, SPEICHERGROESSE);

    // Zur Information erst noch einmal die Kommazahl ausgeben
    printf("Kommazahl  ist: %f\n", kommazahl);

    // Integer aus der Kommazahl "umwandeln", sodass das Bitmuster der IEEE-
    // Zahl als Integer interpretiert wird, und dann ausgeben
    ganzzahl = float_to_int(kommazahl);
    printf("Ganzzahl   ist: %lu\n", ganzzahl);

    // Bitmuster der IEEE-Kommazahl in den reservierten Speicher
    // "bitdarstellung" schreiben und dann ausgeben
    float_to_string(kommazahl, bitdarstellung);
    printf("Bit-String ist: %s\n", bitdarstellung);

    return 0; // Wir waren erfolgreich
}

Mit dem Framework und der darin angegeben Zahl sollte folgende Ausgabe erscheinen:

Kommazahl  ist: 345.214300
Ganzzahl   ist: 4644780690382403718
Bit-String ist: 0100000001110101100100110110110111000101110101100011100010000110

Was jetzt in NASM noch fehlt sind die beiden Funktionen float_to_int und float_to_string. Ihr müsst float_to_string implementieren, ich gebe als Beispiel float_to_int vor:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
; Programm-Bereich in dem wir im vorhinein Speicher reservieren können
section .bss
; Reserviere 1 mal 64bit Speicher
temp:   resq 1

section .text

global float_to_int
float_to_int:

    ; Kopiere den Fließkomma-Parameter in den reservierten Speicher
    movlpd [temp], xmm0
    ; Kopiere die Zahl aus dem Speicher zurück in das Ganzzahl-Register rax
    mov rax, [temp]

    ret

Das eigentliche Programm besteht hier nur aus den Zeilen 12 und 14. Wichtig zu wissen ist, dass die uns bisher bekannten Register (rax, rbx, …, rdi, …) nicht für das Rechnen mit Kommazahlen genutzt werden (können). Dafür gibt es die Register xmm0 bis xmm15. Daher wird der Parameter, der an die Funktion übergeben wird, statt in ein Ganzzahlen-Register in das erste dieser Register (xmm0) geschrieben.

In diesem Register steht jetzt die Zahl als IEEE-Fließkommazahl. Prinzipiell wollen wir die Bitdarstellung komplett übernehmen und nur als Ganzzahl habe. Wir müssen also irgendwie die Bits aus xmm0 in ein Ganzzahlregister (z.B. rax) bekommen.

Ein einfaches mov rax, xmm0 wirft jedoch Fehler, da mov nicht auf die xmm-Register zugreifen kann. Für diese Register gibt es spezielle Befehle unter anderem das Analoge zu mov: movlpd, welches aber nicht auf die Ganzzahl-Register zugreifen kann. Beide Befehle können jedoch in den Speicher schreiben und aus ihm lesen. Daher wenden wir einen kleinen Trick an: Wir kopieren erst die Fließkommazahl in den Speicher (Zeile 12) und kopieren dann die Bits aus dem Speicher in ein Ganzzahl-Register (14). Diese Ganzzahl wird dann zurückgegeben.

Speicherzugriff

Ihr habt euch wahrscheinlich schon gewundert, was diese eckigen Klammern bei [temp] bedeuten. In Zeile 4 werden ja 8 Byte Speicher reserviert und die Adresse des Speichers (was auch nur eine Zahl ist) in temp gespeichert. Würden wir z.B. mov rax, temp ausführen, würde danach in rax die Adresse stehen, die in temp gespeichert ist. Durch die zusätzlichen eckigen Klammern sagen wir: “Nimm nicht die Zahl aus temp als Operand, sondern das Stück Speicher mit der Adresse, die in temp steht.”

1
2
3
4
5
6
7
8
9
10
11
12
    ; Kopiere den Zahlenwert der temp-Adresse nach rdx
    ; Ist die Adresse z.B. 0x343D965F, dann steht danach in rsi diese Zahl
    mov rsi, temp

    ; Kopiere den Zahlenwert, der im Speicher an der temp-Adresse liegt
    ; Steht an der Adresse 0x343D965F z.B. 3436034 im Speicher, dann steht in
    ; rdx jetzt 3436034
    mov rdx, [temp]

    ; Da ja in rsi jetzt die gleiche Adresse steht, wie in temp können wir die
    ; letzte Zeile auch so schreiben
    mov rdx, [rsi]

Zugriff auf von C angelegte Arrays

In der Funktion float_to_string bekommen wir ein char[] übergeben. Das ist nichts anderes als ein Array von chars und ein char ist der C-Datentype für ein Byte. Dieses Array liegt als ein durchgängiger Speicher-Block im Speicher. In unserem Fall ist das Array 65 Byte groß, im Speicher sind also 65 aufeinanderfolgende Byte für das Array reserviert. Der Wert, den wir für dieses Array übergeben bekommen ist die Adresse, des ersten Elements.

[1 Byte][1 Byte][1 Byte] ... [1 Byte]
    ^       ^
    |       Dieses Byte hat dann die Adresse rdi+1
    Die Adresse von diesem Byte wird übergeben (z.B. in rdi)

Kleines Beispiel

array.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>

// Füllt Werte in das Array -- da es direkt auf dem Speicher arbeitet gibt es nichts zurück
void fill_array(char[]);

int main() {
    // Array mit 4 Einträgen anlegen
    char array[4];

    // Array füllen lassen
    fill_array(array);

    // Array ausgeben
    int i;
    for (i = 0; i < 4; i++) {
        printf("array[%d] ist %d\n", i, array[i]);
    }

    return 0;
}

array.asm:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
section .text
global fill_array
fill_array:
    ; Die Adresse wird in rdi übergeben

    ; Wir schreiben in das erste Element eine 46
    ; Wir müssen diese BYTE angeben, damit Prozessor weiß, wie viel Byte wir in
    ; den Speicher schreiben wollen
    mov [rdi], BYTE 46

    ; Wir schreiben in das zweite Element eine -7
    mov [rdi+1], BYTE -7

    ; Wenn wir ein Register benutzen um Bytes zu schreiben, müssen wir uns auf
    ; den 8-Bit-Teil des Registers beschränken, da wir sonst in das nächste
    ; Element auch reinschreiben -- siehe:

    ; Schreibe 10 in letztes Element
    mov [rdi+3], BYTE 10

    ; Soll 5 in drittes Element schreiben - schreibt aber auch 0 ins vierte
    ; Element (sieht Ausgabe des C-Programms - array[3] ist 0 statt 10)
    mov rax, 5
    mov [rdi+2], rax

    ; Korrekt wäre folgendes, da es nur den 8-Bit-Teil des rax-Registers nutzt
    mov al, 5
    mov [rdi+2], al

    ; Um auf das letzte Element zuzugreifen ist auch so etwas möglich:
    ; mov rax, rdi
    ; add rax, 3
    ; mov [rax], BYTE 20

    ret

Ein String in C ist ebenfalls nichts anderes als ein char-Array, welches in jedem Eintrag den ASCII-Code des jeweiligen Zeichens speichert. Wollt ihr also Beispielsweise in ein char-Array der Länge 5 das Wort “Hallo” reinschreiben müssen in die einzelnen Array-Einträge die Zahlen 72, 97, 108, 108, 111 geschrieben werden. Zur Vereinfachung bietet NASM auch an, selber eingegebene Zeichen in ASCII-Code umzuwandeln, es ist statt mov [rbx], BYTE 70 auch möglich mov [rbx], BYTE 'F' zu schreiben.

Das sollte euch erstmal einen groben Überblick auf Speicherzugriffe geben.

Der Ablauf eures Programm sollte also in etwa so aussehen:

  1. Die übergebene Komma-Zahl irgendwie in ein Ganzzahlen-Register bekommen
  2. Die Zahl bitweise durchgehen und für jeden Stelle gucken ob es eine 1 oder eine 0 ist
  3. Dementsprechend in die passende Speicherstelle eine ‘1’ oder ‘0’ reinschreiben

Einzelne Bits testen

Die Operation and ziel, quelle macht ein bitweises logisches Und der beiden Operanden und schreibt das Ergebnis in ziel – Beispiel:

ziel  : 00011001
quelle: 10101111
----------------
=>ziel: 00001001

test op1, op2 macht genau das selbe, schreibt das Ergebnis aber nicht in op1 zurück, sondern setzt nur Statusregister. Für diese Aufgabe könnte das Zero-Flag (ZF mit jz/jnz) interessant sein.

Weiteres werden wird dann im Tutorium am Mittwoch besprechen. Solltet ihr vorher schon Fragen haben, schickt mir eine E-Mail.