Table of Contents

3. Instrukční sada procesoru a přepis algoritmu

Osnova cvičení

  1. Základní instrukce procesoru a jejich popis
  2. Seznámení se simulátorem QtMips
  3. Otestování a implementace jednoduchého sčítání
  4. Přepis programu z jazyka C do assembleru (instrukční sada MIPS)
  5. Práce s periferií

Náplň cvičení

Zjistěte, jak jsou strojové instrukce prováděny/zpracovávány jednoduchým procesorem.

Jednoduchost, přímočarost a ortogonalita kódování instrukcí je základním důvodem, proč si většina učebnic vybírá model MIPS jako model procesorové architektury. Procesor čte instrukce zakódované v binární podobě. Osobní počítače třídy PC nám neumožňují provádět strojový kód založený na MIPS přímo na jejich procesoru (architektura X86), ale existuje mnoho simulátorů této architektury. Simulátor QtMips byl vyvinut jako didaktická pomůcka přímo pro tento kurz.

Základní instrukce - popis a použití

Bližší popis instrukcí:

Instrukce Syntax Operace Význam
add add \$d, \$s, \$t \$d = \$s + \$t; Add: Sečte dva registry \$s + \$t a výsledek uloží do registru \$d
addi addi \$t, \$s, C \$t = \$s + C; Add immediate: Sečte hodnotu v \$s a znaménkově rozšířenou přímou hodnotu, a výsledek uloží do \$t
sub sub \$d,\$s,\$t \$d = \$s - \$t Subtract: Odečte znaménkově obsah registru \$t od \$s a výsledek uloží do \$d
bne bne \$s, \$t, offset if \$s != \$t go to PC+4+4*offset; else go to PC+4 Branch on not equal: Skáče pokud si registry \$s a \$t nejsou rovny
beq beq \$s, \$t, offset if \$s == \$t go to PC+4+4*offset; else go to PC+4 Branch on equal: Skáče pokud si registry \$s a \$t jsou rovny
slt slt \$d,\$s,\$t \$d = (\$s < \$t) Set on less than: Nastavi registr \$d, pokud plati podminka \$s < \$t
sll sll \$d,\$s,C \$d = \$s << C Shift Left Logical: Posune hodnotu v registru o C bitu doleva (ekvivalentni k operaci nasobeni konstantou 2C )
jump j C PC = (PC ∧ 0xf0000000) ∨ 4*C Jump: Skáče bezpodmíněčně na návěstí C
lw lw \$t,C(\$s) \$t = Memory[\$s + C] Load word: Načte slovo z paměti a uloží jej do registru \$t
sw sw \$t,C(\$s) Memory[\$s + C] = \$t Store word: Uloží obsah registru \$t do paměti
lui lui \$t,C \$t = C << 16 Load upper immediate: Uloží předanou přímou hodnotu C do horní části registru. Registr je 32-bitový, C je 16-bitová.
la la \$t, LabelAddr lui \$t, LabelAddr[31:16];
ori \$t,\$t, LabelAddr[15:0]
Load Address: 32-bitové návěstí uloží do registru \$t. Jedná se o pseudoinstrukci - tzn. při překladu se rozloží na dílčí instrukce podle potřeby.
li li \$t, Immediate lui \$t, Immediate[31:16];
ori \$t,\$t, Immediate[15:0]
Load Immediate: 32-bitovou konstantu uloží do registru \$t. Jedná se o pseudoinstrukci shodnou s la - tzn. při překladu se rozloží na dílčí instrukce podle potřeby.

O něco podrobnější popis instrukcí na Wikipedii https://en.wikipedia.org/wiki/MIPS_architecture.

Autoritativní popis architektury https://www.mips.com/:

MIPS32 Instruction Set Quick Reference v1.01

The MIPS32 Instruction Set v6.06

Překlad vlastního zdrojového souboru v assembleru

Pro psaní vlastního zdojového programu v assembleru lze použít libovolný textový textový editor (nainstalované jsou například vim, Emacs, atd. Pro ty, co nemají vlastní preferenci, doporučujeme na začátek editor Geany.

Připravíme jednoduchý zdrojový soubor v assembleru s názvem simple-lw-sw.S. Podstatná je přípona velké “.S”, ta je v standardních vývojových nástrojích vyhrazena pro zdrojový (Source) kód v assembleru a překladač se podle ní rozhoduje, jak bude se souborem nakládat. Další přípony jsou .c pro zdrojový kód v jazyce C, .cc nebo .cpp pro C++, .o (object file) pro objektové soubory (soubory kde je již zdrojový kód přeložen do nativních instrukcí procesoru ale ještě bez finálního umístění na adresy), Knihovní soubory .a (archive) slouží jako knihovny funkcí, které jsou do cílového programu vloženy podle potřeby. Výsledný spustitelný program pak bývá na systémech Unixového typu uložen bez přípony.

.globl _start
.set noat
.set noreorder

.text

_start:
	// load the word from absolute address
	lw     $2, 0x2000($0)
	// store the word to absolute address
	sw     $2, 0x2004($0)

loop:
	// stop execution wait for debugger/user
	break
	// ensure that continuation does not
	// interpret random data
	beq    $0, $0, loop
	nop

.data

src_val:
	.word  0x12345678
dst_val:

Zdrojový kód v assembleru se sestává ze

V kódu jsou použity následující řídicí příkazy

Kompletní popis lze nalézt v manuálu GNU assembleru.

Překlad provedeme křížovým překladačem mips-elf-gcc (křížový proto, že běží na architektuře PC, ale překládá pro architekturu MIPS).

mips-elf-gcc -Wl,-Ttext,0x1000 -Wl,-Tdata,0x2000 -nostdlib -nodefaultlibs -nostartfiles -o simple-lw-sw simple-lw-sw.S

Volání obsahuje množství parametrů, protože běžné programy v jazyce C jsou doplněné o inicializační sekvence a knihovní funkce. Účelem našeho výkladu je nejdříve ukázat, jak pracuje úplný základ a poté pochopit, jak může být obalen a rozšířen automaticky pracující inicializace a konstrukce, které umožňují pohodlné psaní programů na vyšší úrovni i ve vyšších jazycích. Volby -Wl, jsou přidávané sestavovacímu programu (linkeru) a určují, na jaké adresy bude umístěna sekce s instrukcemi .text a datová sekce .data. Další volby postupně zakazují vložení standardní startovací sekvence (-nostartfiles) a automatické použití knihoven. Volba -o a následující argument určují jméno výstupního souboru (output). Dále následuje jeden nebo více zdrojových souborů.

Zdrojový soubor k projektu simple-lw-sw a příslušné instrukce pro kompilaci (Makefile) jsou dostupné v adresáři /opt/apo/qtmips-semstart.

Soubor buble_sort.S

Pro kompilaci interním překladačem QtMIPS využijte tuto šablonu buble_sort.S:

// Directives to make interresting windows visible
#pragma qtmips show registers
#pragma qtmips show memory

.globl _start
.set noat
.set noreorder

.text

_start:
          lui $8,0x2
.data
.org	0x20200

src_val:
	.word  100,20,200,50,45,30,2,58,70,35

// Specify location to show in memory window
#pragma qtmips focus memory src_val

Pro překlad z C do assembleru zkuste soubor init_array.c:

int pole[10];
int _start() {
  int N = 10,i;
  for(i=0; i<N; i++) {
    pole[i]=N-i;
  }
  return 0;
}

Překlad do assembleru pro MIPS a zobrazení přeložených instrukcí provedeme následujícími příkazy.

mips-elf-gcc -nostdlib -g init_array.c -o init_array
mips-elf-objdump --source init_array

Na Windows je možné nainstalovat buď kompletní MSys s make nebo používat jen holý kompilátor mips-elf-gcc-i686-mingw32 a dávkový soubor.

PATH=%PATH%;c:\path\to\mips-elf-gcc-i686-mingw32\bin
mips-elf-gcc -Wl,-Ttext,0x1000 -Wl,-Tdata,0x2000 -nostdlib -nodefaultlibs -nostartfiles -o simple-lw-sw test.S

Pro provedení programu použijeme simulátor QtMips. Volíme nejednodušší variantu procesoru “No pipeline no cache”. Do pole “Elf executable” vložíme po vyhledání přes tlačítko “Browse” zkompilovaný soubor simple-lw-sw (bez přípony).

Dále v záložce dialogového okna Core vypneme položku Delay slot. Tím se sice simulátor odkloní od reálného chování architektury MIPS, ale návrh programů v režimu set .noreorder bude přímočaře odpovídat předpokládanému chování skokových instrukcí. Později se k problematice vrátíme.

V zobrazeném diagramu dvojklikem na čítači instrukcí (PC) otevřeme okno s programem. Dvojklikem na bloku zápisníkové paměti (Registers) je otevřený náhled na soubor registrů a nakonec dvojklikem na datech dojde k zobrazení paměti. Doporučené rozložení oken může být následující

V okně “Program” navolíme “Follow fetch”, kdy je vždy v okně vybraná právě procesorem načtená instrukce/řádka. V okně “Memory” nastavíme adresu 0x2000 na kterou byla umístěna data 0x12345678. Program krokujeme příkazem v menu “Machine” → “Step” nebo z nástrojové lišty příslušným tlačítkem. Hodnota je nejdříve načtena do registru a poté zapsaná do paměti o jednu buňku dále.

Nyní upravte program tak, aby prováděl kopírování periodicky. Při úpravách bude potřeba vypustit instrukci break, která slouží k zastavení/přechodu do ladícího stavu. Vyzkoušejte, že modifikovaná hodnota na adrese 0x2000 je vždy překopírovaná na adresu 0x2004. Modifikujete program tak, aby sčítal dvě vstupní hodnoty na adresách 0x2000 a 0x2004 a výsledek ukládal na adresu 0x2008.

Dále program upravte tak, aby sčítal dva vektory o délce čtyř slov. Využijte k nastavení registru na počáteční adresu vektoru makroinstrukce assembleru la vect_a (load address).

	...
.data

vect_a:
	.word  0x12345678
	.word  0x12345678
	.word  0x12345678
	.word  0x12345678
vect_b:
	.word  0x12345678
	...

Dále navrhněte program pro počítání průměru z osmi čísel.

Použití Makefile pro řízení překladu

Volání kompilátoru je vhodné minimálně dokumentovat a lépe zautomatizovat. Jednou z možností je vytvořit skript pro shell (na GNU/Linuxu obvykle BASH nebo DASH). Při překladu větších projektů je ale nepraktické překládat všechny kompilační jednotky opakovaně, když dojde k malé změně jen v některém ze zdrojových souborů. proto bylo vytvořeno množství programů speciálně navržených pro účel překladu a sestavení programu (Make, Ant, qmake, Cmake, meson, atd.).

Makefile

Make je nástroj pro automatickou kompilaci zdrojových kódů, potřebná konfigurace je uložena v souboru Makefile.

Makefile šablona pro překlad zdrojového kódu v assembleru:

ARCH=mips-elf

CC=$(ARCH)-gcc
AS=$(ARCH)-as
LD=$(ARCH)-ld
OBJCOPY=$(ARCH)-objcopy

CFLAGS  += -ggdb -O1
AFLAGS  += -ggdb
LDFLAGS += -ggdb
LDFLAGS += -nostdlib -nodefaultlibs -nostartfiles
LDFLAGS += -Wl,-Ttext,0x1000 -Wl,-Tdata,0x2000

all:default

.PHONY:clean

%.srec:%
        $(OBJCOPY) -O srec $< $@

%.o:%.S
        $(CC) -D__ASSEMBLY__ $(AFLAGS) -c $< -o $@

%.s:%.c
        $(CC) $(CFLAGS) $(CPPFLAGS) -S $< -o $@

%.o:%.c
        $(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@

# default output
default:change_me.srec

# executable file:object file.o
change_me:change_me.o
        $(CC) $(LDFLAGS) $^ -o $@

# all generated that would be cleaned
clean:
        rm -f change_me change_me.o change_me.out change_me.srec

Odsazení řádku v Makefile musí být pomocí tabulátoru. Makefile nerozezná odsazení pomocí mezer.

Sestavovací soubor Makefile se sestává z definic (přiřazení hodnot proměnným) a pravidel. Pravidla začínají řádkou, která určuje závislost cílů na jejich závislostech uvedených za dvojtečkou. Pravidla mohou přímo uvádět jména souborů nebo mohou být obecná, kdy zástupným znakem pro doplňovanou část jména je znak procento “%”. Kompletnější šablonu pro překlad na cílovou architekturu MIPS i s nalezením závislostí na hlavičkových souborech naleznete na počítačích v laboratoři v adresáři /opt/apo/qtmips_template.

Kompilace

Kompilace probíhá příkazem make (příkaz make je nutné zadávat v adresáři, ve kterém je upravený Makefile a zdrojový kód programu). Make vygeneruje několik souborů, z nichž pro import do simulačního prostředí QtMips použijeme soubor bez koncovky. Překlad nejdříve převádí kompilační jednotky (jednotka je zjednodušeně jeden zdrojový soubor včetně vložených hlavičkových souborů) do objektového tvaru (.o) v relokovatelné podobě. Objektové soubory jsou linkerem složeny, vzájemné reference/odkazy vyřešeny a provedeno umístění na finální adresy. Pro uložení vlastních instrukcí strojového kódu je potřeba přidat obálku s informacemi, kde je potřeba kód upravit při konečném umístění .o souborů. Data a instrukce v konečném spustitelném tvaru je také potřeba doplnit informací na které adresy má být daná část/sekce umístěna. K uložení těchto informací je v našem případě a obecně na většině standardům odpovídajících systémů použitý formát ELF (Executable and Linkable Format).

Zjištění adres proměnných a korespondence zdrojového kódu a výsledného ELF souboru

Zjištění adres, na které byly jednotlivé funkce a datové položky linkerem umístěny lze zjistit

mips-elf-nm program

Rozmístění sekcí

mips-elf-objdump --headers program

Porovnání vstupu a výsledku překladu

mips-elf-objdump --source program

Přepis programu z jazyka C do asembleru

V praktických aplikacích se častokrát setkáváme s použitím mediánového filtru. Ten nám pomáhá odstranit ze signálu (nebo obrazu) zcela zjevné výkmity (nebo poškozené pixely). Narozdíl od průměrovacího filtru, který spočítá aritmetický průměr nejakého okolí a stávající hodnotu signálu nahradí vypočteným průměrem, mediánový filtr ji nahradí prostřední hodnotou (mediánem) tohoto okolí. Pro realizaci mediánového filtru je potřebné nejdříve seřadit všechny hodnoty a pak z nich vybrat onu prostřední. Klíčovou roli zde sehrává řazení. Mějme následující problém. V datové paměti nech je uloženo N celých čísel (1 < N < 21) počínaje od nějaké adresy (například 0x00), přičemž jedno číslo v paměti zabírá velikost jednoho slova. Našim úkolem je uvedená čísla vzestupně seřadit. Nejsnažší způsob jak tento problém řešit je použít bublinkové řazení. Princip tohoto algoritmu spočívá v tom, že se postupně a opakovaně prochází seřazované pole, přičemž se porovnávají každé dva sousedící prvky, a pokud nejsou ve správném pořadí, prohodí se. Hodnotu N si zvolte sami.

int pole[5]={5,3,4,1,2};
int main()
{
	int N = 5,i,j,tmp;
	for(i=0; i<N; i++)
		for(j=0; j<N-1-i; j++)
			if(pole[j+1]<pole[j])
			{
				tmp = pole[j+1];
				pole[j+1] = pole[j];
				pole[j] = tmp;
			}
	return 0;
}

Princip algoritmu: Řazení prvků se uskutečňuje ve dvou vnořených cyklech - vnějším a vnitřním. Vnější cyklus zabezpečuje opakované spouštění vnitřního cyklu. Vnitřní cyklus postupně procházení pole a vzájemně vyměňuje dvě sousedící položky pokud nejsou v správném pořadí (má být menší vlevo, větší napravo). Protože vnitřní cyklus prochází polem zleva doprava (od menších indexů k větším) důsledkem je, že na konci pole se objeví vždy největší prvek z celého pole - největší prvek “probublal” na konec pole. Proto při dalším spuštění vnitřního cyklu nemusíme již procházet celým polem, ale jenom doposud neseřazenou částí tohoto pole. Postačuje N spuštění vnitřního cyklu aby se pole seřadilo.

5, 3, 4, 1, 2 –> počáteční stav
3, 4, 1, 2, 5 –> po prvním průchodu vnějším cyklem
3, 1, 2, 4, 5 –> po druhém průchodu vnějším cyklem
1, 2, 3, 4, 5
1, 2, 3, 4, 5
1, 2, 3, 4, 5 –> po pátém průchodu cyklem - seřazeno

Přepište výše uvedený program z jazyka C do asembleru. Vykonávání programu oveřte v simulátoru Mips. Tento program budete potřebovat na příštím cvičení - to co nestihnete na cvičeních bude nutno dodělat doma..


Můžete využít předpřipravenou šablonu k tomuto úkolu:

.globl pole
.globl start

.set noat
.set noreorder

.text
.ent start

start:
// Zde muzete psat Vas program
nop
.end start

.data
.align    2

pole:
.word    5,3,4,1,2

Příklady přepisu krátkých fragmentů kódu z jazyka C do asembleru

Příkaz if
if (i ==j)
  f = g + h;
 
f = f – i;
//   s0=f, s1=g, s2=h, s3=i, s4=j

  bne $s3, $s4, L1   // Pokud i!=j, skoč na L1 
  add $s0, $s1, $s2   // if blok: f=g+h
L1:
  sub $s0, $s0, $s3   // f = f-i
Příkaz if-else
if (i ==j)
  f = g + h;
else
  f = f – i;
//   s0=f, s1=g, s2=h, s3=i, s4=j

  bne $s3, $s4, else  // Když i!=j, skoč na else
  add $s0, $s1, $s2    // if blok: f=g+h
  j L2              // přeskoč blok else
else:
  sub $s0, $s0, $s3    // blok else: f = f-i
L2:
Cyklus while
int pow = 1;
int x = 0;
 
while(pow != 128)
{
  pow = pow*2;
  x = x + 1;
}
// s0=pow, s1=x

  addi $s0, $0, 1     // pow = 1
  addi $s1, $0, 0     // x = 0
  addi $t0, $0, 128   // t0 = 128 pro porovnávání

while:
  beq  $s0, $t0, done  // Když pow==128, ukončení cyklu while
  sll  $s0, $s0, 1     // pow = pow*2
  addi $s1, $s1, 1     // x = x+1
  j    while
done:
Cyklus for
int sum = 0;
int i;
for(i=0; i!=10; i++)
{
  sum = sum + i;
}
//Lze nahradit while:
int sum = 0;
int i = 0;
while(i!=10)
{ sum = sum + i;
  i++;
}
//ale zde i rychlejším do-while, 
//z konst.mezí víme,že se vždy vykoná:
int sum = 0;
int i = 0;
do
{ sum = sum + i;
  i++;
} while(i!=10)
Načtení slova z datové paměti
// Jenom pro účely ukázky...
int a, *pa=0x80020040;
int b, *pb=0x80020044;
int c, *pc=0x00001234;
 
a = *pa;
b = *pb;
c = *pc;
// s0=pa (bazova adresa), s1=a, s2=b, s3=c

lui  $s0, 0x8002   // pa = 0x80020000;
lw   $s1, 0x40($s0)     // a = *pa;
lw   $s2, 0x44($s0)     // b = *pb;

addi $s0, $0, 0x1234   // pc = 0x00001234;
lw   $s3, 0x0($s0)      // c = *pc;
Inkrementování prvků pole
int pole[4] = { 7, 2, 3, 5 };
 
int main()
{
   int i,tmp;
   for(i=0; i<4; i++)
   {
      tmp = pole[i];
      tmp += 1;
      pole[i] = tmp;
   }
   return 0;
}
Kompletní program v prostředí QtMips:
.globl    pole        // nazev "pole" bude viditelny pro linker
.data                 // direktiva oznacujici zacatek datove casti
.align    2           // zarovnani dat po slovech (4 Bytech)

pole:                 // pojmenovani mista v pameti
.word    7, 2, 3, 5   // inicializace pole...

.text                 // zacatek textove casti / programu
.globl _start	      // opet symbol viditelný pro linker	  
.ent _start	      // vstupní bod		  

_start:               // la je pseudoinstrukce
la   $s0, pole         // ulozi do registru s0 adresu pocatku pole
addi $s1, $0, 0        // inicializacni prikaz cyklu for: i=0, kde i=s1
addi $s2, $0, 4        // nastaveni horni meze cyklu

for:                  // mame konst. meze, lze prelozit jako do-while 
  lw   $s3, 0x0($s0)    // nacteni polozky pole do registru s3
  addi $s3, $s3, 0x1    // inkrementace registru s3
  sw   $s3, 0x0($s0)    // prepsani (ulozeni) hodnoty registru s3 do pole
  addi $s0, $s0, 0x4    // posun na dasli polozku pole
  addi $s1, $s1, 0x1    // +1 k pocitadlu poctu pruchodu cyklem (i++)
  bne  $s1, $s2, for    // kdyz s1!=s2 opakuj cyklus skokem na navesti for

konec:
  nop
.end _start


Periferie mapované do paměťového adresního prostoru

Simulátor QtMips nabízí i několik jednoduchých periferií, které jsou mapované do paměťového adresního prostoru.

První periferií je jednoduchý sériový port (UART) napojený na okénko terminálu. Rozložení a funkce registrů odpovídá emulátorům SPIM a MARS. Ty mapují sériový port od adresy 0xffff0000. V simulátoru QtMips je mapovaný jak na tuto adresu, tak na adresu 0xffffc000, která je dosažitelná absolutním adresováním v instrukcích LW a SW proti registru zero.

Adresa Symbolické označení Bity Popis
0xffffc000 SERP_RX_ST_REG Stavový registr přijímače znaků z terminálu
0 Pokud je 1 tak je v SERP_RX_DATA_REG nový přijatý znak
1 Natavením na 1 povoluje přerušení od příjmu podrobněji
0xffffc004 SERP_RX_DATA_REG 7 .. 0 ASCII kód přijatého znaku
0xffffc008 SERP_TX_ST_REG Stavový registr vysílače znaků na terminál
0 Pokud je 1 tak je vysílač připravený na zápis znaku
1 Natavením na 1 povoluje přerušení od vysílání podrobněji
0xffffc00c SERP_TX_DATA_REG 7 .. 0 ASCII kód vysílaného znaku

Další periferie emuluje interakci s reálnými prvky zařízení. Tato periferie přesně odpovídá rozložením registrů a bitů zjednodušené periferii otočných voličů a LED indikátorů, která je použitá pro vstup a výstup na vývojových kitech MicroZed APO, na které budou použité v semestrálních úlohách.

Adresa Symbolické označení Bity Popis
0xffffc104 SPILED_REG_LED_LINE 31 .. 0 Slovo zobrazené binárně, dekadicky a hexadecimálně
0xffffc110 SPILED_REG_LED_RGB1 23 .. 0 Zápis RGB hodnot do PWM registrů pro RGB LED 1
23 .. 16 Červená složka R
15 .. 8 Zelená složka G
7 .. 0 Modrá složka B
0xffffc114 SPILED_REG_LED_RGB2 23 .. 0 Zápis RGB hodnot do PWM registrů pro RGB LED 2
23 .. 16 Červená složka R
15 .. 8 Zelená složka G
7 .. 0 Modrá složka B
0xffffc124 SPILED_REG_KNOBS_8BIT 31 .. 0 Filtrované hodnoty voličů jako 8 bit čísla
7 .. 0 Nastavení modrého voliče B
15 .. 8 Nastavení zeleného voliče G
23 .. 16 Nastavení natočení červeného voliče R

Příklad na práci s perifériemi

.globl _start
.ent _start
.set noat
.set noreorder

.text
_start:
    li $8, 0xffffc100   // base address into memory mapped I/O area
loop:
    lw $9, 0x24($8)     // load packed knob value into $9 (from addr. 0xffffc124)
    
    // depack $9 and store individual knob values into registers
    andi $12, $9, 0xFF  // $12 <-blue knob is in bits 7..0
    srl  $1, $9, 8      // $11 <-green knob is in bits 15..8
    andi $11, $1, 0xFF  
    srl  $1, $9, 16     // $10 <-red knob is in bits 23..16
    andi $10, $1, 0xFF  
    
    sw $9, 0x10($8) //write packed knobs to RGB led_1,(to addr. 0xffffc110)
    sw $9, 4($8)    // and also to LED-line word-box of QtMips,(to addr. 0xffffc104)    
    
    // bit negation of the packed knob value
    addi $1, $0, -1  // $1 = 0xffffffff
    xor $1,$1,$9     // xor with all 1 performs the bit negation of $9
            
    sw $1, 0x14($8)     // write negated value of the packed knob to RGB led_2
                        // (to addr. 0xffffc114)
    
    beq $0, $0, loop    // repeat rd/wr in endless loop
    nop

.end _start

Analýza výsledku kompilace

Jednoduchý program pro čtení polohy otočných voličů a převodu hodnoty na barvu a textový výstup naleznete na laboratorních počítačích v adresáři /opt/apo/binrep/qtmips_binrep. Archiv ke stažení je k dispozici zde. Můžete také použít šablony z repozitáře https://gitlab.fel.cvut.cz/b35apo/stud-support v adresari seminaries/binrep

Zdojový kód je zkompilovaný sekvencí příkazů

mips-elf-gcc -D__ASSEMBLY__ -ggdb -fno-lto -c crt0local.S -o crt0local.o
mips-elf-gcc -ggdb -Os -Wall -fno-lto  -c qtmips_binrep.c -o qtmips_binrep.o
mips-elf-gcc -ggdb -nostartfiles -static -fno-lto crt0local.o qtmips_binrep.o -o qtmips_binrep

Následně je obsah binárního výstup ve formátu ELF převedený do textové podoby příkazem

mips-elf-objdump --source -M no-aliases,reg-names=numeric qtmips_binrep

A doplněný o komentáře.

qtmips_binrep:     file format elf32-bigmips


Disassembly of section .text:

00400018 <main>:

/*
 * The main entry into example program
 */
int main(int argc, char *argv[])
{
  400018:	27bdffe8 	addiu	$29,$29,-24
                           allocate space on the stack for main() function
                           stack frame
  
  40001c:	afbf0014 	sw	$31,20($29)
                           save previous value of the return address register
                           to the stack.

 while (1) {
     uint32_t rgb_knobs_value;
     unsigned int uint_val;

      rgb_knobs_value = *(volatile uint32_t*)(mem_base + SPILED_REG_KNOBS_8BIT_o);
  400020:	8c04c124 	lw	$4,-16092($0)
                           Read value from the address corresponding to the
                           sum of "SPILED_REG_BASE" and "SPILED_REG_KNOBS_8BIT_o"
                           peripheral register offset
                           LW is instruction to load the word. Address is formed
			   from the sum of register $0 (fixed zero) and -16092,
			   which is represented in hexadecimal as 0xffffc124
			   i.e., sum of 0xffffc100 and 0x24. The read value is
			   stored in register $4.

  400024:	00000000 	sll	$0,$0,0x0
                           one NOP instruction to ensure that load finishes before
                           the further value use.

  400028:	00041027 	nor	$2,$0,$4
                           Compute bit complement "~" of the value in the register
			   $4 and store it into register $2

     *(volatile uint32_t*)(mem_base + SPILED_REG_LED_LINE_o) = rgb_knobs_value;
  40002c:	ac04c104 	sw	$4,-16124($0)
                           Store RGB knobs values from register $4to the "LED"
			   line register which is shown in binary decimal
			   and hexadecimal on the QtMips target.
			   Address 0xffffc104

     *(volatile uint32_t*)(mem_base + SPILED_REG_LED_RGB1_o) = rgb_knobs_value;
  400030:	ac04c110 	sw	$4,-16112($0)
                           Store RGB knobs values to the corresponding components
			   controlling a color/brightness of the RGB LED 1
                           Address 0xffffc110


     *(volatile uint32_t*)(mem_base + SPILED_REG_LED_RGB2_o) = ~rgb_knobs_value;
  400034:	ac02c114 	sw	$2,-16108($0)
                           Store complement of RGB knobs values to the corresponding
			   components controlling a color/brightness of the RGB LED 2
                           Address 0xffffc114

     /* Assign value read from knobs to the basic signed and unsigned types */
     uint_val = rgb_knobs_value;
                           the read value resides in the register 4, which
                           correspond to the first argument register a0

     /* Print values */
     serp_send_hex(uint_val);
  400038:	0c100028 	jal	4000a0 <serp_send_hex>
  40003c:	00000000 	sll	$0,$0,0x0
                           call the function to send hexadecimal value to
                           the serial port, one instruction after JAL
                           is executed in its delay-slot, PC pointing
                           after this instruction (0x400040) is stored
                           to the register 31, return address register

     serp_tx_byte('\n');
  400040:	0c100020 	jal	400080 <serp_tx_byte>
  400044:	2404000a 	addiu	$4,$0,10
                           call routine to send new line character to the
                           serial port. The ASCII value corresponding to
                           '\n' is set to argument a0 register in delay slot
                           of JAL. JAL is decoded and in parallel instruction
                           addiu $4,$0,10 is executed then PC pointing to the address
                           0x400048 after delay slot is stored to return address
                           register and next instruction is fetch from the JAL
                           instruction target address, start of the function
                           serp_tx_byte

  400048:	1000fff5 	beqz	$0,400020 <main+0x8>
  40004c:	00000000 	sll	$0,$0,0x0
                           branch back to the start of the loop reading value from
                           the knobs


00400050 <_start>:
	la      $gp, _gp
  400050:	3c1c0041 	lui	$28,0x41
  400054:	279c90e0 	addiu	$28,$28,-28448
                           Load global data base pointer to the global data
                           base register 28 - gp.
			   Symbol _gp is provided by linker.

	addi    $a0, $zero, 0
  400058:	20040000 	addi	$4,$0,0
                           Set regist a0 (the first main function argument)
			   to zero, argc is equal to zero.

	addi    $a1, $zero, 0
  40005c:	20050000 	addi	$5,$0,0
                           Set regist a1 (the second main function argument)
			   to zero, argv is equal to NULL.

	jal     main
  400060:	0c100006 	jal	400018 <main>
	nop
  400064:	00000000 	sll	$0,$0,0x0
                           Call the main function. Return address is stored
			   in the ra ($31) register.


00400068 <quit>:
quit:
	addi    $a0, $zero, 0
  400068:	20040000 	addi	$4,$0,0
                           If the main functio returns, set exit value to 0

	addi    $v0, $zero, 4001  /* SYS_exit */
  40006c:	20020fa1 	addi	$2,$0,4001
                           Set system call number to code representing exit()

	syscall
  400070:	0000000c 	syscall
                           Call the system.

00400074 <loop>:

loop:	break
  400074:	0000000d 	break
                           If there is not a system try to stop the execution
			   by invoking debugging exception

        beq     $zero, $zero, loop
  400078:	1000fffe 	beqz	$0,400074 <loop>
	nop
  40007c:	00000000 	sll	$0,$0,0x0
                           If even this does not stop execution, command CPU
			   to spin in busy loop.

void serp_tx_byte(int data)
{
00400080 <serp_tx_byte>:
  while (!(serp_read_reg(SERIAL_PORT_BASE, SERP_TX_ST_REG_o) &
                SERP_TX_ST_REG_READY_m));
  400080:	8c02c008 	lw	$2,-16376($0)
  400084:	00000000 	sll	$0,$0,0x0
                           Read serial port transmit status register,
                           address 0xffffc008

  while (!(serp_read_reg(SERIAL_PORT_BASE, SERP_TX_ST_REG_o) &
  400088:	30420001 	andi	$2,$2,0x1
  40008c:	1040fffc 	beqz	$2,400080 <serp_tx_byte>
  400090:	00000000 	sll	$0,$0,0x0
                           Wait again till UART is ready to accept
			   character - bit 0 is not zero.
                           NOP in the delayslot.

  *(volatile uint32_t *)(base + reg) = val;
  400094:	ac04c00c 	sw	$4,-16372($0)
                           write value from register 4 (the first argument a0)
                           to the address 0xffffc00c (SERP_TX_DATA_REG_o)
                           serial port tx data register.
}
  400098:	03e00008 	jr	$31
  40009c:	00000000 	sll	$0,$0,0x0
                           jump/return back to continue in callee program
                           address of the next fetch instruction is read
                           from the return address register 32 ra
                           
void serp_send_hex(unsigned int val)
{
004000a0 <serp_send_hex>:
  4000a0:	27bdffe8 	addiu	$29,$29,-24
                           allocate space on the stack for the routine stack frame
                                                      
  4000a4:	00802825 	or	$5,$4,$0
                           copy value of the fisrt argument regsiter 4 (a0)
                           to the register 5

  for (i = 8; i > 0; i--) {
  4000a8:	24030008 	addiu	$3,$0,8
                           set the value of the register 3 to the 8

  4000ac:	afbf0014 	sw	$31,20($29)
                           save previous value of the return address register
                           to the stack.

    char c = (val >> 28) & 0xf;
  4000b0:	00051702 	srl	$2,$5,0x1c
                           shift value in register 5 right by 28 bits and store
                           result in the register 2

  4000b4:	304600ff 	andi	$6,$2,0xff
                           abundant operation to limit value range to the character
                           type variable and store result in the register 6
    if (c < 10 )
  4000b8:	2c42000a 	sltiu	$2,$2,10
                           set register 2 to one if the value is smaller than 10

      c += 'A' - 10;
  4000bc:	10400002 	beqz	$2,4000c8 <serp_send_hex+0x28>
  4000c0:	24c40037 	addiu	$4,$6,55
                           if value is larger or equal (register 2 is 0/false) then add
                           value 55 ('A' - 10)..(0x41 - 0xa) = 0x37 = 55 to the register
                           6 and store result in the register 4. This operation is
                           executed even when the branch arm before else is executed,
                           but result is immediately overwritten by next instruction
      c += '0';
  4000c4:	24c40030 	addiu	$4,$6,48
                           add value 0x30 = 48 = '0' to the value in the register 6
                           and store result in the register 4 - the fisrt argument a0
  
    serp_tx_byte(c);
  4000c8:	0c100020 	jal	400080 <serp_tx_byte>
  4000cc:	2463ffff 	addiu	$3,$3,-1
                           call subroutine to send byte to the serial port
                           decrement loop control variable (i) in delay-slot

  for (i = 8; i > 0; i--) {
  4000d0:	1460fff7 	bnez	$3,4000b0 <serp_send_hex+0x10>
  4000d4:	00052900 	sll	$5,$5,0x4
                           the final condition of for loop converted to do {} while()
                           loop. If not all 8 character send loop again.
                           Shift left value in the register 5 by 4 bit positions.
                           The compiler does not store values of local variables to
                           the stack even does not store values in caller save registers
                           (which requires to save previous values to the function stack frame).
                           Compiler can use this optimization because it knows registers usage
                           of called function serp_tx_byte().
  }
  4000d8:	8fbf0014 	lw	$31,20($29)
  4000dc:	00000000 	sll	$0,$0,0x0
                           restore return address register value to that found at function
                           start
                           
  4000e0:	03e00008 	jr	$31
  4000e4:	27bd0018 	addiu	$29,$29,24
                           return to the caller function. Instruction in jump register
                           delay-slot is used to restore stack pointer/free function frame.

Úkoly

  1. Seznámení se simulátorem a vývojovým řetězcem
  2. Rozšíření jednoduchého programu na sčítání čísel
  3. Práce s vektorem
  4. Implementace výpočtu n-tého členu Fibonacciho řady
  5. Začátek práce na programu bubble-sort
  6. Výpis řetězce na terminál
  7. Doplnění programu pro výpočet Fibonacciho posloupnosti o výstup na terminál

Odkazy

gcc -E assembler.S -o predzpracovany-pro-mips.s