Spis treści
Transkrypt
Spis treści
Spis treści Rozdział 1. Wstęp do języka asemblerowego...................................................... 13 1.1. Wprowadzenie........................................................................................................ 13 1.2. Anatomia programu HLA....................................................................................... 13 1.3. Uruchamianie pierwszego programu HLA ............................................................. 16 1.4. Podstawowe deklaracje danych programu HLA..................................................... 17 1.5. Wartości logiczne ................................................................................................... 19 1.6. Wartości znakowe................................................................................................... 20 1.7. Rodzina procesorów 80x86 firmy Intel .................................................................. 21 1.7.1. Podsystem obsługi pamięci........................................................................... 24 1.8. Podstawowe instrukcje maszynowe........................................................................ 26 1.9. Podstawowe struktury sterujące wykonaniem programu HLA............................... 30 1.9.1. Wyrażenia logiczne w instrukcjach HLA ..................................................... 31 1.9.2. Instrukcje if..then..elseif..else..endif języka HLA ......................................... 33 1.9.3. Iloczyn, suma i negacja w wyrażeniach logicznych...................................... 35 1.9.4. Instrukcja while ............................................................................................ 37 1.9.5. Instrukcja for................................................................................................. 38 1.9.6. Instrukcja repeat............................................................................................ 39 1.9.7. Instrukcje break oraz breakif......................................................................... 40 1.9.8. Instrukcja forever.......................................................................................... 40 1.9.9. Instrukcje try, exception oraz endtry............................................................. 41 1.10. Biblioteka standardowa języka HLA — wprowadzenie ......................................... 44 1.10.1. Stałe predefiniowane w module stdio ........................................................... 46 1.10.2. Standardowe wejście i wyjście programu ..................................................... 46 1.10.3. Procedura stdout.newln................................................................................. 47 1.10.4. Procedury stdout.putiN ................................................................................. 47 1.10.5. Procedury stdout.putiNSize .......................................................................... 48 1.10.6. Procedura stdout.put ..................................................................................... 49 1.10.7. Procedura stdin.getc...................................................................................... 51 1.10.8. Procedury stdin.getiN ................................................................................... 52 1.10.9. Procedury stdin.readLn i stdin.flushInput..................................................... 53 1.10.10. Procedura stdin.get ....................................................................................... 54 1.11. Jeszcze o ochronie wykonania kodu w bloku try..endtry........................................ 55 1.11.1. Zagnieżdżone bloki try..endtry ..................................................................... 56 1.11.2. Klauzula unprotected bloku try..endtry......................................................... 58 1.11.3. Klauzula anyexception bloku try..endtry ...................................................... 61 1.11.4. Instrukcja try..endtry i rejestry...................................................................... 61 1.12. Język asemblerowy a język HLA ........................................................................... 63 1.13. Źródła informacji dodatkowych.............................................................................. 64 4 Asembler. Sztuka programowania Rozdział 2. Reprezentacja danych ...................................................................... 65 2.1. Wprowadzenie........................................................................................................ 65 2.2. Systemy liczbowe ................................................................................................... 66 2.2.1. System dziesiętny — przypomnienie............................................................ 66 2.2.2. System dwójkowy......................................................................................... 66 2.2.3. Formaty liczb dwójkowych........................................................................... 68 2.3. System szesnastkowy.............................................................................................. 69 2.4. Organizacja danych ................................................................................................ 72 2.4.1. Bity ............................................................................................................... 72 2.4.2. Półbajty......................................................................................................... 73 2.4.3. Bajty ............................................................................................................. 73 2.4.4. Słowa ............................................................................................................ 75 2.4.5. Podwójne słowa ............................................................................................ 76 2.4.6. Słowa poczwórne i długie............................................................................. 77 2.5. Operacje arytmetyczne na liczbach dwójkowych i szesnastkowych....................... 77 2.6. Jeszcze o liczbach i ich reprezentacji...................................................................... 78 2.7. Operacje logiczne na bitach.................................................................................... 81 2.8. Operacje logiczne na liczbach dwójkowych i ciągach bitów.................................. 84 2.9. Liczby ze znakiem i bez znaku ............................................................................... 86 2.10. Rozszerzanie znakiem, rozszerzanie zerem, skracanie, przycinanie....................... 91 2.11. Przesunięcia i obroty .............................................................................................. 95 2.12. Pola bitowe i dane spakowane................................................................................ 99 2.13. Wprowadzenie do arytmetyki zmiennoprzecinkowej ........................................... 104 2.13.1. Formaty zmiennoprzecinkowe przyjęte przez IEEE ................................... 108 2.13.2. Obsługa liczb zmiennoprzecinkowych w języku HLA ............................... 112 2.14. Reprezentacja liczb BCD ..................................................................................... 115 2.15. Znaki .................................................................................................................... 117 2.15.1. Zestaw znaków ASCII................................................................................ 117 2.15.2. Obsługa znaków ASCII w języku HLA...................................................... 121 2.16. Zestaw znaków Unicode....................................................................................... 125 2.17. Źródła informacji dodatkowych............................................................................ 126 Rozdział 3. Dostęp do pamięci i jej organizacja ................................................ 127 3.1. Wprowadzenie...................................................................................................... 127 3.2. Tryby adresowania procesorów 80x86 ................................................................. 127 3.2.1. Adresowanie przez rejestr........................................................................... 128 3.2.2. 32-bitowe tryby adresowania procesora 80x86........................................... 129 3.3. Organizacja pamięci fazy wykonania ................................................................... 135 3.3.1. Obszar kodu................................................................................................ 137 3.3.2. Obszar zmiennych statycznych................................................................... 139 3.3.3. Obszar niemodyfikowalny .......................................................................... 140 3.3.4. Obszar danych niezainicjalizowanych ........................................................ 141 3.3.5. Atrybut @nostorage.................................................................................... 141 3.3.6. Sekcja deklaracji var ................................................................................... 142 3.3.7. Rozmieszczenie sekcji deklaracji danych w programie HLA ..................... 143 3.4. Przydział pamięci dla zmiennych w programach HLA......................................... 144 3.5. Wyrównanie danych w programach HLA ............................................................ 146 3.6. Wyrażenia adresowe............................................................................................. 149 3.7. Koercja typów ...................................................................................................... 151 3.8. Koercja typu rejestru ............................................................................................ 154 3.9. Pamięć obszaru stosu oraz instrukcje push i pop .................................................. 155 3.9.1. Podstawowa postać instrukcji push............................................................. 155 3.9.2. Podstawowa postać instrukcji pop .............................................................. 157 3.9.3. Zachowywanie wartości rejestrów za pomocą instrukcji push i pop .......... 158 Spis treści 5 3.9.4. Stos jako kolejka LIFO ............................................................................... 159 3.9.5. Pozostałe wersje instrukcji obsługi stosu.................................................... 161 3.9.6. Usuwanie danych ze stosu bez ich zdejmowania ........................................ 163 3.9.7. Odwoływanie się do danych na stosie bez ich zdejmowania ...................... 165 3.10. Dynamiczny przydział pamięci — obszar pamięci sterty ..................................... 166 3.11. Instrukcje inc oraz dec.......................................................................................... 171 3.12. Pobieranie adresu obiektu..................................................................................... 171 3.13. Źródła informacji dodatkowych............................................................................ 172 Rozdział 4. Stałe, zmienne i typy danych.......................................................... 173 4.1. Wprowadzenie...................................................................................................... 173 4.2. Kilka dodatkowych instrukcji: intmul, bound i into ............................................. 174 4.3. Typ tbyte............................................................................................................... 178 4.4. Deklaracje stałych i zmiennych w języku HLA.................................................... 178 4.4.1. Typy stałych ............................................................................................... 182 4.4.2. Literały stałych łańcuchowych i znakowych............................................... 183 4.4.3. Stałe łańcuchowe i napisowe w sekcji const............................................... 185 4.4.4. Wyrażenia stałowartościowe....................................................................... 186 4.4.5. Wielokrotne sekcje const i ich kolejność w programach HLA ................... 189 4.4.6. Sekcja val programu HLA .......................................................................... 190 4.4.7. Modyfikowanie obiektów sekcji val w wybranym miejscu kodu źródłowego programu ....................................................................... 191 4.5. Sekcja type programu HLA .................................................................................. 192 4.6. Typy wyliczeniowe w języku HLA ...................................................................... 193 4.7. Typy wskaźnikowe ............................................................................................... 194 4.7.1. Wskaźniki w języku asemblerowym........................................................... 196 4.7.2. Deklarowanie wskaźników w programach HLA ........................................ 197 4.7.3. Stałe wskaźnikowe i wyrażenia stałych wskaźnikowych............................ 197 4.7.4. Zmienne wskaźnikowe a dynamiczny przydział pamięci ........................... 199 4.7.5. Typowe błędy stosowania wskaźników ...................................................... 200 4.8. Moduł chars.hhf biblioteki standardowej HLA .................................................... 205 4.9. Złożone typy danych ............................................................................................ 207 4.10. Łańcuchy znaków................................................................................................. 208 4.11. Łańcuchy w języku HLA...................................................................................... 210 4.12. Odwołania do poszczególnych znaków łańcucha ................................................. 217 4.13. Moduł strings biblioteki standardowej HLA i procedury manipulacji łańcuchami...219 4.14. Konwersje wewnątrzpamięciowe ......................................................................... 231 4.15. Zbiory znaków...................................................................................................... 232 4.16. Implementacja zbiorów znaków w języku HLA................................................... 233 4.17. Literały, stałe i wyrażenia zbiorów znaków w języku HLA ................................. 235 4.18. Operator in w wyrażeniach logicznych wysokopoziomowego rozszerzenia języka HLA.................................................. 237 4.19. Obsługa zbiorów znaków w bibliotece standardowej HLA.................................. 237 4.20. Wykorzystywanie zbiorów znaków w programach HLA ..................................... 241 4.21. Tablice.................................................................................................................. 243 4.22. Deklarowanie tablic w programach HLA ............................................................. 244 4.23. Literały tablicowe................................................................................................. 245 4.24. Odwołania do elementów tablicy jednowymiarowej ............................................ 246 4.24.1. Porządkowanie tablicy wartości ................................................................. 248 4.25. Tablice wielowymiarowe ..................................................................................... 250 4.25.1. Wierszowy układ elementów tablicy .......................................................... 251 4.25.2. Kolumnowy układ elementów tablicy ........................................................ 255 4.26. Przydział pamięci dla tablic wielowymiarowych ................................................. 256 4.27. Odwołania do elementów tablic wielowymiarowych w języku asemblerowym... 258 6 Asembler. Sztuka programowania 4.28. Duże tablice i MASM (tylko dla programistów systemu Windows) .................... 259 4.29. Rekordy (struktury) .............................................................................................. 260 4.30. Stałe rekordowe.................................................................................................... 263 4.31. Tablice rekordów.................................................................................................. 264 4.32. Wykorzystanie tablic i rekordów w roli pól rekordów ......................................... 265 4.33. Ingerowanie w przesunięcia pól rekordów ........................................................... 269 4.34. Wyrównanie pól w ramach rekordu...................................................................... 270 4.35. Wskaźniki na rekordy........................................................................................... 271 4.36. Unie ...................................................................................................................... 273 4.37. Unie anonimowe................................................................................................... 275 4.38. Typy wariantowe .................................................................................................. 276 4.39. Stałe unii............................................................................................................... 277 4.40. Przestrzenie nazw ................................................................................................. 278 4.41. Tablice dynamiczne w języku asemblerowym ..................................................... 281 4.42. Obsługa tablic w bibliotece standardowej języka HLA ........................................ 284 4.43. Źródła informacji dodatkowych............................................................................ 287 Rozdział 5. Procedury i moduły ........................................................................ 289 5.1. Wprowadzenie...................................................................................................... 289 5.2. Procedury.............................................................................................................. 289 5.3. Zachowywanie stanu systemu .............................................................................. 292 5.4. Przedwczesny powrót z procedury ....................................................................... 296 5.5. Zmienne lokalne ................................................................................................... 297 5.6. Symbole lokalne i globalne obiektów innych niż zmienne................................... 303 5.7. Parametry.............................................................................................................. 304 5.7.1. Przekazywanie przez wartość ..................................................................... 305 5.7.2. Przekazywanie przez adres ......................................................................... 308 5.8. Funkcje i wartości funkcji .................................................................................... 311 5.8.1. Zwracanie wartości funkcji......................................................................... 312 5.8.2. Złożenie instrukcji języka HLA .................................................................. 313 5.8.3. Atrybut @returns procedur języka HLA..................................................... 316 5.9. Rekurencja............................................................................................................ 318 5.10. Deklaracje zapowiadające .................................................................................... 322 5.11. Procedury w ujęciu niskopoziomowym — instrukcja call.................................... 323 5.12. Rola stosu w procedurach..................................................................................... 325 5.13. Rekordy aktywacji................................................................................................ 328 5.14. Standardowa sekwencja wejścia do procedury ..................................................... 331 5.15. Standardowa sekwencja wyjścia z procedury....................................................... 333 5.16. Niskopoziomowa implementacja zmiennych automatycznych ............................ 334 5.17. Niskopoziomowa implementacja parametrów procedury..................................... 336 5.17.1. Przekazywanie argumentów w rejestrach ................................................... 337 5.17.2. Przekazywanie argumentów w kodzie programu........................................ 340 5.17.3. Przekazywanie argumentów przez stos....................................................... 342 5.18. Wskaźniki na procedury ....................................................................................... 365 5.19. Parametry typu procedurowego............................................................................ 368 5.20. Nietypowane parametry wskaźnikowe ................................................................. 370 5.21. Zarządzanie dużymi projektami programistycznymi............................................ 371 5.22. Dyrektywa #include ............................................................................................. 372 5.23. Unikanie wielokrotnego włączania do kodu tego samego pliku........................... 374 5.24. Moduły a atrybut @external................................................................................. 375 5.24.1. Działanie atrybutu @external ..................................................................... 380 5.24.2. Pliki nagłówkowe w programach HLA....................................................... 382 5.25. Jeszcze o problemie zaśmiecania przestrzeni nazw .............................................. 384 5.26. Źródła informacji dodatkowych............................................................................ 386 Spis treści 7 Rozdział 6. Arytmetyka.................................................................................... 389 6.1. Wprowadzenie...................................................................................................... 389 6.2. Zestaw instrukcji arytmetycznych procesora 80x86 ............................................. 389 6.2.1. Instrukcje mul i imul................................................................................... 389 6.2.2. Instrukcje div i idiv..................................................................................... 393 6.2.3. Instrukcja cmp ............................................................................................ 396 6.2.4. Instrukcje setXX ......................................................................................... 401 6.2.5. Instrukcja test.............................................................................................. 403 6.3. Wyrażenia arytmetyczne ...................................................................................... 404 6.3.1. Proste przypisania ....................................................................................... 405 6.3.2. Proste wyrażenia ......................................................................................... 406 6.3.3. Wyrażenia złożone...................................................................................... 408 6.3.4. Operatory przemienne................................................................................. 413 6.4. Wyrażenia logiczne .............................................................................................. 414 6.5. Idiomy maszynowe a idiomy arytmetyczne.......................................................... 417 6.5.1. Mnożenie bez stosowania instrukcji mul, imul i intmul.............................. 417 6.5.2. Dzielenie bez stosowania instrukcji div i idiv............................................. 419 6.5.3. Zliczanie modulo n za pośrednictwem instrukcji and ................................. 420 6.5.4. Nieostrożne korzystanie z idiomów maszynowych .................................... 420 6.6. Arytmetyka zmiennoprzecinkowa ........................................................................ 421 6.6.1. Rejestry jednostki zmiennoprzecinkowej.................................................... 421 6.6.2. Typy danych jednostki zmiennoprzecinkowej ............................................ 429 6.6.3. Zestaw instrukcji jednostki zmiennoprzecinkowej ..................................... 430 6.6.4. Instrukcje przemieszczania danych............................................................. 431 6.6.5. Instrukcje konwersji.................................................................................... 433 6.6.6. Instrukcje arytmetyczne .............................................................................. 436 6.6.7. Instrukcje porównań ................................................................................... 442 6.6.8. Instrukcje ładowania stałych na stos koprocesora....................................... 445 6.6.9. Instrukcje funkcji przestępnych .................................................................. 445 6.6.10. Pozostałe instrukcje jednostki zmiennoprzecinkowej ................................. 447 6.6.11. Instrukcje operacji całkowitoliczbowych.................................................... 449 6.7. Tłumaczenie wyrażeń arytmetycznych na kod maszynowy jednostki zmiennoprzecinkowej........................................................................... 449 6.7.1. Konwersja notacji wrostkowej do odwrotnej notacji polskiej..................... 451 6.7.2. Konwersja odwrotnej notacji polskiej do kodu języka asemblerowego...... 453 6.8. Obsługa arytmetyki zmiennoprzecinkowej w bibliotece standardowej języka HLA.....455 6.8.1. Funkcje stdin.getf i fileio.getf ..................................................................... 455 6.8.2. Funkcje trygonometryczne modułu math.................................................... 455 6.8.3. Funkcje wykładnicze i logarytmiczne modułu math................................... 456 6.9. Podsumowanie...................................................................................................... 458 Rozdział 7. Niskopoziomowe struktury sterujące wykonaniem programu............ 459 7.1. Wprowadzenie...................................................................................................... 459 7.2. Struktury sterujące niskiego poziomu................................................................... 460 7.3. Etykiety instrukcji................................................................................................. 460 7.4. Bezwarunkowy skok do instrukcji (instrukcja jmp) ............................................. 462 7.5. Instrukcje skoku warunkowego ............................................................................ 465 7.6. Struktury sterujące „średniego” poziomu — jt i jf................................................ 468 7.7. Implementacja popularnych struktur sterujących w języku asemblerowym ......... 469 7.8. Wstęp do podejmowania decyzji .......................................................................... 469 7.8.1. Instrukcje if..then..else................................................................................ 471 7.8.2. Tłumaczenie instrukcji if języka HLA na język asemblerowy.................... 475 7.8.3. Obliczanie wartości złożonych wyrażeń logicznych — metoda pełnego szacowania wartości wyrażenia................................... 480 8 Asembler. Sztuka programowania 7.8.4. Skrócone szacowanie wyrażeń logicznych ................................................. 481 7.8.5. Wady i zalety metod szacowania wartości wyrażeń logicznych ................. 483 7.8.6. Efektywna implementacja instrukcji if w języku asemblerowym ............... 485 7.8.7. Instrukcje wyboru ....................................................................................... 490 7.9. Skoki pośrednie a automaty stanów...................................................................... 500 7.10. Kod spaghetti........................................................................................................ 503 7.11. Pętle...................................................................................................................... 504 7.11.1. Pętle while .................................................................................................. 505 7.11.2. Pętle repeat..until ........................................................................................ 506 7.11.3. Pętle nieskończone ..................................................................................... 508 7.11.4. Pętle for ...................................................................................................... 508 7.11.5. Instrukcje break i continue.......................................................................... 509 7.11.6. Pętle a rejestry ............................................................................................ 513 7.12. Optymalizacja kodu.............................................................................................. 514 7.12.1. Obliczanie warunku zakończenia pętli na końcu pętli ................................ 515 7.12.2. Zliczanie licznika pętli wstecz .................................................................... 517 7.12.3. Wstępne obliczanie niezmienników pętli.................................................... 518 7.12.4. Rozciąganie pętli ........................................................................................ 519 7.12.5. Zmienne indukcyjne ................................................................................... 521 7.13. Mieszane struktury sterujące w języku HLA........................................................ 522 7.14. Źródła informacji dodatkowych............................................................................ 524 Rozdział 8. Pliki .............................................................................................. 525 8.1. Wprowadzenie...................................................................................................... 525 8.2. Organizacja plików............................................................................................... 525 8.2.1. Pliki jako listy rekordów............................................................................. 526 8.2.2. Pliki tekstowe a pliki binarne...................................................................... 528 8.3. Pliki sekwencyjne ................................................................................................. 530 8.4. Pliki dostępu swobodnego .................................................................................... 538 8.5. Indeksowany sekwencyjny dostęp do pliku (ISAM) ............................................ 543 8.6. Przycinanie pliku .................................................................................................. 546 8.7. Źródła informacji dodatkowych............................................................................ 548 Rozdział 9. Zaawansowane obliczenia w języku asemblerowym......................... 549 9.1. Wprowadzenie...................................................................................................... 549 9.2. Operacje o zwielokrotnionej precyzji ................................................................... 550 9.2.1. Obsługa operacji zwielokrotnionej precyzji w bibliotece standardowej języka HLA...................................................... 550 9.2.2. Dodawanie liczb zwielokrotnionej precyzji ................................................ 553 9.2.3. Odejmowanie liczb zwielokrotnionej precyzji............................................ 556 9.2.4. Porównanie wartości o zwielokrotnionej precyzji ...................................... 558 9.2.5. Mnożenie operandów zwielokrotnionej precyzji ........................................ 562 9.2.6. Dzielenie wartości zwielokrotnionej precyzji ............................................. 565 9.2.7. Negacja operandów zwielokrotnionej precyzji ........................................... 575 9.2.8. Iloczyn logiczny operandów zwielokrotnionej precyzji.............................. 577 9.2.9. Suma logiczna operandów zwielokrotnionej precyzji................................. 577 9.2.10. Suma wyłączająca operandów zwielokrotnionej precyzji........................... 578 9.2.11. Inwersja operandów zwielokrotnionej precyzji .......................................... 578 9.2.12. Przesunięcia bitowe operandów zwielokrotnionej precyzji ........................ 578 9.2.13. Obroty operandów zwielokrotnionej precyzji............................................. 583 9.2.14. Operandy zwielokrotnionej precyzji w operacjach wejścia-wyjścia........... 583 9.3. Manipulowanie operandami różnych rozmiarów.................................................. 604 9.4. Arytmetyka liczb dziesiętnych.............................................................................. 606 9.4.1. Literały liczb BCD...................................................................................... 608 9.4.2. Instrukcje maszynowe daa i das.................................................................. 608 Spis treści 9 9.4.3. Instrukcje maszynowe aaa, aas, aam i aad .................................................. 610 9.4.4. Koprocesor a arytmetyka spakowanych liczb dziesiętnych ........................ 612 9.5. Obliczenia w tabelach........................................................................................... 615 9.5.1. Wyszukiwanie w tabeli wartości funkcji .................................................... 615 9.5.2. Dopasowywanie dziedziny ......................................................................... 620 9.5.3. Generowanie tabel wartości funkcji............................................................ 621 9.5.4. Wydajność odwołań do tabel przeglądowych ............................................. 625 9.6. Źródła informacji dodatkowych............................................................................ 625 Rozdział 10. Makrodefinicje i język czasu kompilacji .......................................... 627 10.1. Wprowadzenie...................................................................................................... 627 10.2. Język czasu kompilacji — wstęp .......................................................................... 627 10.3. Instrukcje #print i #error....................................................................................... 629 10.4. Stałe i zmienne czasu kompilacji.......................................................................... 631 10.5. Wyrażenia i operatory czasu kompilacji............................................................... 632 10.6. Funkcje czasu kompilacji ..................................................................................... 635 10.6.1. Funkcje czasu kompilacji — konwersja typów........................................... 636 10.6.2. Funkcje czasu kompilacji — obliczenia numeryczne ................................. 638 10.6.3. Funkcje czasu kompilacji — klasyfikacja znaków ..................................... 638 10.6.4. Funkcje czasu kompilacji — manipulacje łańcuchami znaków .................. 639 10.6.5. Funkcje czasu kompilacji — dopasowywanie wzorców............................. 639 10.6.6. Odwołania do tablicy symboli .................................................................... 641 10.6.7. Pozostałe funkcje czasu kompilacji ............................................................ 643 10.6.8. Konwersja typu stałych napisowych........................................................... 643 10.7. Kompilacja warunkowa........................................................................................ 645 10.8. Kompilacja wielokrotna (pętle czasu kompilacji)................................................. 650 10.9. Makrodefinicje (procedury czasu kompilacji) ...................................................... 653 10.9.1. Makrodefinicje standardowe....................................................................... 654 10.9.2. Argumenty makrodefinicji.......................................................................... 656 10.9.3. Symbole lokalne makrodefinicji ................................................................. 663 10.9.4. Makrodefinicje jako procedury czasu kompilacji ....................................... 666 10.9.5. Symulowane przeciążanie funkcji .............................................................. 667 10.10. Tworzenie programów czasu kompilacji.............................................................. 672 10.10.1. Generowanie tabel wartości funkcji............................................................ 673 10.10.2. Rozciąganie pętli ........................................................................................ 677 10.11. Stosowanie makrodefinicji w osobnych plikach kodu źródłowego ...................... 679 10.12. Źródła informacji dodatkowych ........................................................................... 679 Rozdział 11. Manipulowanie bitami .................................................................... 681 11.1. Wprowadzenie...................................................................................................... 681 11.2. Czym są dane bitowe? .......................................................................................... 681 11.3. Instrukcje manipulujące bitami............................................................................. 683 11.4. Znacznik przeniesienia w roli akumulatora bitów ................................................ 692 11.5. Wstawianie i wyodrębnianie łańcuchów bitów .................................................... 693 11.6. Scalanie zbiorów bitów i rozpraszanie łańcuchów bitowych................................ 696 11.7. Spakowane tablice łańcuchów bitowych .............................................................. 699 11.8. Wyszukiwanie bitów ............................................................................................ 701 11.9. Zliczanie bitów ..................................................................................................... 704 11.10. Odwracanie łańcucha bitów.................................................................................. 707 11.11. Scalanie łańcuchów bitowych .............................................................................. 709 11.12. Wyodrębnianie łańcuchów bitów ......................................................................... 710 11.13. Wyszukiwanie wzorca bitowego .......................................................................... 712 11.14. Moduł bits biblioteki standardowej HLA ............................................................. 713 11.15. Źródła informacji dodatkowych ........................................................................... 715 10 Asembler. Sztuka programowania Rozdział 12. Operacje łańcuchowe..................................................................... 717 12.1. Wprowadzenie...................................................................................................... 717 12.2. Instrukcje łańcuchowe procesorów 80x86............................................................ 717 12.2.1. Sposób działania instrukcji łańcuchowych ................................................. 718 12.2.2. Przedrostki instrukcji łańcuchowych — repX ............................................ 719 12.2.3. Znacznik kierunku ...................................................................................... 719 12.2.4. Instrukcja movs........................................................................................... 721 12.2.5. Instrukcja cmps........................................................................................... 727 12.2.6. Instrukcja scas............................................................................................. 731 12.2.7. Instrukcja stos............................................................................................. 732 12.2.8. Instrukcja lods ............................................................................................ 733 12.2.9. Instrukcje lods i stos w złożonych operacjach łańcuchowych .................... 733 12.3. Wydajność instrukcji łańcuchowych procesorów 80x86 ...................................... 734 12.4. Źródła informacji dodatkowych............................................................................ 735 Rozdział 13. Instrukcje MMX............................................................................. 737 13.1. Wprowadzenie...................................................................................................... 737 13.2. Sprawdzanie obecności rozszerzenia MMX......................................................... 738 13.3. Środowisko programowania MMX ...................................................................... 739 13.3.1. Rejestry MMX............................................................................................ 739 13.3.2. Typy danych MMX .................................................................................... 741 13.4. Przeznaczenie instrukcji zestawu MMX............................................................... 742 13.5. Arytmetyka z nasycaniem a arytmetyka z zawijaniem ......................................... 743 13.6. Operandy instrukcji MMX ................................................................................... 744 13.7. Instrukcje zestawu MMX ..................................................................................... 746 13.7.1. Instrukcje transferu danych......................................................................... 747 13.7.2. Instrukcje konwersji.................................................................................... 747 13.7.3. Arytmetyka operandów spakowanych ........................................................ 752 13.7.4. Instrukcje logiczne...................................................................................... 755 13.7.5. Instrukcje porównań ................................................................................... 756 13.7.6. Instrukcje przesunięć bitowych................................................................... 760 13.7.7. Instrukcja emms.......................................................................................... 762 13.8. Model programowania MMX............................................................................... 763 13.9. Źródła informacji dodatkowych............................................................................ 774 Rozdział 14. Klasy i obiekty............................................................................... 775 14.1. Wprowadzenie...................................................................................................... 775 14.2. Wstęp do programowania obiektowego................................................................ 775 14.3. Klasy w języku HLA ............................................................................................ 779 14.4. Obiekty ................................................................................................................. 782 14.5. Dziedziczenie ....................................................................................................... 784 14.6. Przesłanianie......................................................................................................... 785 14.7. Metody wirtualne a procedury statyczne .............................................................. 786 14.8. Implementacje metod i procedur klas ................................................................... 788 14.9. Implementacja obiektu ......................................................................................... 793 14.9.1. Tabela metod wirtualnych .......................................................................... 796 14.9.2. Reprezentacja w pamięci obiektu klasy pochodnej..................................... 798 14.10. Konstruktory i inicjalizacja obiektów................................................................... 802 14.10.1. Konstruktor a dynamiczny przydział obiektu ............................................. 804 14.10.2. Konstruktory a dziedziczenie...................................................................... 806 14.10.3. Parametry konstruktorów i przeciążanie procedur klas .............................. 810 14.11. Destruktory........................................................................................................... 811 14.12. Łańcuchy _initialize_ oraz _finalize_ w języku HLA .......................................... 812 14.13. Metody abstrakcyjne ............................................................................................ 818 Spis treści 11 14.14. Informacja o typie czasu wykonania (RTTI) ........................................................ 822 14.15. Wywołania metod klasy bazowej ......................................................................... 824 14.16. Źródła informacji dodatkowych ........................................................................... 825 Rozdział 15. Na styku asemblera i innych języków programowania...................... 827 15.1. Wprowadzenie...................................................................................................... 827 15.2. Łączenie kodu HLA i kodu asemblera MASM bądź Gas ..................................... 827 15.2.1. Kod asemblera MASM (Gas) rozwijany w kodzie języka HLA................. 828 15.2.2. Konsolidacja modułów MASM (Gas) z modułami HLA............................ 832 15.3. Moduły HLA a programy języka Delphi (Kylix) ................................................. 837 15.3.1. Konsolidacja modułów HLA z programami języka Delphi (Kylix) ........... 838 15.3.2. Zachowywanie wartości rejestrów.............................................................. 842 15.3.3. Wartości zwracane funkcji.......................................................................... 843 15.3.4. Konwencje wywołań .................................................................................. 849 15.3.5. Przekazywanie argumentów przez wartość i adres, parametry niemodyfikowalne i wyjściowe ................................................. 854 15.3.6. Skalarne typy danych w językach HLA i Delphi (Kylix) ........................... 856 15.3.7. Przekazywanie łańcuchów znaków z Delphi do procedury HLA ............... 858 15.3.8. Przekazywanie rekordów z programu w języku Delphi do kodu HLA....... 862 15.3.9. Przekazywanie zbiorów z programu w języku Delphi do kodu HLA ......... 866 15.3.10. Przekazywanie tablic z programu w języku Delphi do kodu HLA ............. 867 15.3.11. Odwołania do obiektów programu pisanego w Delphi w kodzie HLA....... 867 15.4. Moduły HLA a programy języków C i C++ ......................................................... 870 15.4.1. Konsolidacja modułów języka HLA z programami języków C i C++........ 872 15.4.2. Zachowywanie wartości rejestrów.............................................................. 875 15.4.3. Wartości funkcji ......................................................................................... 876 15.4.4. Konwencje wywołań .................................................................................. 876 15.4.5. Tryby przekazywania argumentów ............................................................. 880 15.4.6. Odwzorowanie typów skalarnych pomiędzy językiem C (C++) a językiem HLA ........................................... 881 15.4.7. Przekazywanie łańcuchów znaków pomiędzy programem w języku C (C++) a modułem HLA........................ 883 15.4.8. Przekazywanie rekordów pomiędzy programem w języku C (C++) a modułem HLA........................ 883 15.4.9. Przekazywanie tablic pomiędzy programem w języku C (C++) a modułem HLA........................ 886 15.5. Źródła informacji dodatkowych............................................................................ 886 Dodatek A Tabela kodów ASCII....................................................................... 887 Dodatek B Instrukcje procesorów 80x86......................................................... 891 Skorowidz...................................................................................... 927 1.9. Podstawowe struktury sterujące wykonaniem programu HLA Instrukcje , oraz , choć niezwykle użyteczne, nie wystarczają do napisania sensownych programów. Aby takie programy mogły powstać, podstawowe instrukcje muszą zostać uzupełnione o podstawowy zbiór struktur sterujących wykonaniem kodu umożliwiających tworzenie pętli i podejmowanie decyzji w programie. Język HLA przewiduje szereg wysokopoziomowych struktur sterujących wykonaniem charakterystycznych dla języków wyższego niż asembler poziomu. Programista HLA ma więc do dyspozycji konstrukcje , , i tak dalej. Dopiero znajomość i umiejętność wykorzystywania owych konstrukcji pozwala na stworzenie prawdziwego programu. Przed zagłębieniem się w cechach struktur sterujących należałoby podkreślić, że nie mają one odpowiedników w zestawie instrukcji maszynowych procesorów 80x86. Kompilator HLA tłumaczy owe struktury do postaci szeregu prawdziwych instrukcji maszynowych za programistę. Sposób, w jaki to czyni, opisany zostanie w dalszej części książki; wtedy też Czytelnik nauczy się pisać kod w czystym języku asemblerowym, bez wykorzystania wysokopoziomowych cech kompilatora HLA. Na razie jednak, z racji niewielkiego jeszcze zasobu wiedzy o języku asemblerowym, nieco sztuczne struktury sterujące HLA będą bardzo pomocne. Warto przy tym pamiętać, że wysokopoziomowe na pierwszy rzut oka struktury sterujące nie są w istocie tak wysokopoziomowe. Mają one bowiem jedynie ułatwić rozpoczęcie pisania programów w języku asemblerowym — w żadnym razie nie zastępują asemblerowych instrukcji sterujących wykonaniem kodu. Już wkrótce okaże się więc, że owe wysokopoziomowe struktury sterujące mają szereg ograniczeń, które w miarę nabierania przez Czytelnika doświadczenia i w miarę komplikowania kolejnych programów staną się dość uciążliwe. To zresztą efekt zamierzony — po osiągnięciu pewnego poziomu umiejętności programista sam stwierdzi, że w wielu przypadkach wygoda (i czytelność programu) wynikająca z zastosowania struktur sterujących HLA nie rekompensuje utraty efektywności, charakterystycznej dla kodu wykorzystującego wprost instrukcje maszynowe procesora. Niniejsze omówienie kierowane jest do tych Czytelników, którzy znają już przynajmniej jeden z popularnych języków wysokiego poziomu. Znajomość ta znakomicie uprości prezentację struktur sterujących wykonaniem programu HLA, pozwalając na pominięcie opisów sposobów zastosowania owych struktur w typowych zadaniach programistycznych. Czytelnicy potrafiący korzystać ze struktur sterujących wykonaniem programu w dowolnym z języków wysokiego poziomu nie powinni być zaskoczeni, kiedy odkryją, że analogiczne struktury wykorzystywane są w programach HLA w sposób dosłownie identyczny. Rozdział 1. ♦ Wstęp do języka asemblerowego 31 1.9.1. Wyrażenia logiczne w instrukcjach HLA Niektóre z instrukcji sterujących wykonaniem programu HLA wymagają określenia w miejsce operandu wartości logicznej („prawda” bądź „fałsz”); ścieżka wykonania programu zależy wtedy od wartości wyrażenia logicznego. Przykładami struktur wymagających określenia wyrażenia logicznego są instrukcje , oraz . Pierwszą oznaką ograniczeń struktur sterujących wykonaniem programu HLA jest składnia owych wyrażeń. W tym jednym miejscu nawyki wyniesione z języków wysokiego poziomu zwracają się przeciwko programiście asemblera: w języku HLA wyrażenia logiczne są bardzo ograniczone — nie sposób konstruować tu wyrażeń równie wymyślnych, z jakimi może mieć do czynienia programista języka C++. Wyrażenia logiczne w języku HLA mogą przyjąć jedną z następujących postaci1: Znacznik może zostać określony za pośrednictwem jednego z symboli wyliczonych w tabeli 1.2. Tabela 1.2. Symbole reprezentujące znaczniki 1 Symbol Znacznik Znaczenie Przeniesienie „Prawda” dla ustawionego (1) znacznika przeniesienia; „fałsz” dla wyzerowanego (0) znacznika przeniesienia. Brak przeniesienia „Prawda” dla wyzerowanego (0) znacznika przeniesienia; „fałsz” dla ustawionego (1) znacznika przeniesienia. Zero „Prawda” dla ustawionego (1) znacznika zera; „fałsz” dla wyzerowanego (0) znacznika zera. Brak zera „Prawda” dla wyzerowanego (0) znacznika zera; „fałsz” dla ustawionego (1) znacznika zera. Przepełnienie „Prawda” dla ustawionego (1) znacznika przepełnienia; „fałsz” dla wyzerowanego (0) znacznika przepełnienia. Brak przepełnienia „Prawda” dla wyzerowanego (0) znacznika przepełnienia; „fałsz” dla ustawionego (1) znacznika przepełnienia. Znak „Prawda” dla ustawionego (1) znacznika znaku; „fałsz” dla wyzerowanego (0) znacznika znaku. Brak znaku „Prawda” dla wyzerowanego (0) znacznika znaku; „fałsz” dla ustawionego (1) znacznika znaku. Istnieje jeszcze kilka postaci dodatkowych; zostaną one omówione w dalszych podrozdziałach i rozdziałach. 32 Asembler. Sztuka programowania Wykorzystanie tych znaczników w wyrażeniach logicznych to już dość zaawansowane programowanie w asemblerze. Sposób konstruowania wyrażeń logicznych angażujących znaczniki słowa stanu procesora zaprezentowany zostanie w następnym rozdziale. W przypadku, kiedy wartość logiczna angażuje operand rejestrowy, operand ten może odnosić się do zarówno do rejestru 8-bitowego, 16-bitowego, jak i 32-bitowego. Wyrażenie takie przyjmuje wartość „fałsz”, jeśli rejestr zawiera wartość zero. Dla każdej innej wartości rejestru wyrażenie logiczne przyjmuje wartość „prawda”. Jeśli w wyrażeniu logicznym uwikłana jest zmienna przechowywana w pamięci, jej wartość jest sprawdzana, a wyrażenie przyjmuje wartość logiczną „prawdy” bądź „fałszu” w zależności od tej wartości, według reguł identycznych jak dla rejestrów. Należy jednak pamiętać, że w wyrażeniu logicznym powinna występować zmienna typu logicznego (). Uwikłanie w wyrażeniu logicznym zmiennej innego typu spowoduje błąd. W przypadku potrzeby określenia wartości logicznej zmiennej typu innego niż należy skorzystać z wyrażeń logicznych ogólnego przeznaczenia, omówionych poniżej. Najbardziej ogólna postać wyrażenia logicznego w języku HLA pozwala na określenie operatora relacji wraz z dwoma operandami. Listę dozwolonych kombinacji rodzajów operandów i operatorów relacji zawiera tabela 1.3. Tabela 1.3. Dozwolone wyrażenia logiczne Operand lewy Operator relacji Operand prawy Zmienna w pamięci bądź rejestr albo Zmienna w pamięci, rejestr bądź stała albo Nie można określić obu operandów jako operandów pamięciowych. Lewy operand można więc utożsamić z operandem źródłowym, a operand prawy z operandem docelowym i stosować kombinacje rodzajów operandów takie jak dla instrukcji czy . Kolejna analogia operatorów relacji do instrukcji i objawia się w wymogu identycznego rozmiaru operandów. I tak, oba operandy muszą być albo bajtami, albo słowami, albo podwójnymi słowami. Jeśli operand prawy jest stałą, jego wartość musi nadawać się do zapisania w operandzie lewym. I jeszcze jedno: jeśli lewy operand jest rejestrem, a prawy to stała dodatnia albo inny rejestr, porównanie odbywa się bez uwzględnienia znaku operandów. Efekty takiego porównania omówione zostaną w następnym rozdziale; na razie wystarczy zapamiętać, że nie powinno się porównywać wartości ujemnej przechowywanej w rejestrze z wartością stałą albo zawartością innego rejestru. Wynik porównania może bowiem odbiegać od oczekiwań programisty. Rozdział 1. ♦ Wstęp do języka asemblerowego 33 Operatory i pozwalają na wykonanie testu zawierania się wartości przechowywanej w rejestrze w określonym zbiorze liczb. Na przykład wyrażenie daje wartość logiczną , jeśli wartość przechowywana w rejestrze EAX to wartość z zakresu od 2000 do 2099 (włącznie). Operator ma działanie odwrotne — da wartość logiczną jedynie wtedy, kiedy zawartość rejestru to liczba spoza określonego zbioru. Na przykład wyrażenie będzie miało wartość tylko wtedy, kiedy znak przechowywany w rejestrze AL nie będzie małą literą alfabetu. Oto kilka przykładów wyrażeń logicznych w języku HLA: !"" #$$$ $ 1.9.2. Instrukcje if..then..elseif..else..endif języka HLA Składnia instrukcji warunkowego wykonania kodu w wydaniu języka HLA prezentowana jest na rysunku 1.10. Rysunek 1.10. Składnia instrukcji if w języku HLA Wyrażenia występujące w instrukcji muszą odpowiadać jednej z postaci wyrażeń logicznych, zaprezentowanych w poprzednim ustępie. Jeśli wyrażenie przyjmie wartość logiczną „prawda”, wykonany zostanie kod umieszczony za klauzulą . W przeciwnym przypadku sterowanie przekazane zostanie do kodu w ramach klauzuli bądź . 34 Asembler. Sztuka programowania Ponieważ klauzule i nie są obowiązkowe, instrukcja w najprostszej postaci składa się z wyrażenia logicznego i pojedynczej klauzuli , za którą występuje instrukcja lub blok instrukcji wykonywanych warunkowo, w zależności od wartości wyrażenia logicznego: %& !"'# ()*)&+,(-./011+2'3 (%3 Jeśli w trakcie wykonania programu wyrażenie logiczne w instrukcji będzie miało wartość , wykonany zostanie kod znajdujący się pomiędzy słowami i . W przypadku kiedy wyrażenie logiczne będzie miało wartość , kod ów zostanie pominięty. Inną powszechnie występującą formą instrukcji jest forma z pojedynczą klauzulą . Instrukcja taka wygląda następująco: %& !"'# ()*)&+,(-./011.4 4+2'3 ()*)&+5*...4 4+2'3 (%3 Jeśli wyrażenie logiczne przyjmie wartość , wykonany zostanie kod zawarty między słowami a . W przypadku wartości sterowanie przekazane zostanie do kodu ograniczonego klauzulą i słowem . Przez osadzanie opcjonalnych klauzul w instrukcji można rozbudowywać podejmowanie decyzji o wykonaniu kodu. Na przykład, jeśli rejestr CH zawiera wartość znakową, można na podstawie jego wartości dokonać wyboru spośród dostępnych opcji menu, konstruując następującą instrukcję : %&#$$'# ()*)&+67,* 78 9 )-$$+2'3 %&#$,$'# ()*)&+67,* 78 9 )-$,$+2'3 %&#$$' ()*)&+67,* 78 9 )-$$+2'3 ()*)&+(-.7,*8* 9 )+2'3 (%3 Rozdział 1. ♦ Wstęp do języka asemblerowego 35 Choć nie widać tego w powyższym przykładzie, język HLA nie wymaga umieszczania po sekwencji klauzul klauzuli . Jednak przy podejmowaniu decyzji o wykonaniu jednej z wielu ścieżek kodu warto uzupełnić instrukcję klauzulą rozpoczynającą ścieżkę kodu obsługi ewentualnych błędów wyboru. Nawet jeśli pozornie wykonanie owego kodu wydaje się niemożliwe, to należy wziąć pod uwagę ewentualny rozwój aplikacji — w kolejnych wersjach kodu wybór ścieżki wykonania może mieć inny przebieg; warto, aby wtedy kod decyzyjny uzupełniony został o obsługę sytuacji wyjątkowych i niespodziewanych. 1.9.3. Iloczyn, suma i negacja w wyrażeniach logicznych Na liście operatorów zaprezentowanej w poprzednich punktach zabrakło bardzo ważnych operatorów logicznych: operatora iloczynu logicznego (AND), logicznej sumy (OR) oraz logicznej negacji (NOT). Ich zastosowanie w wyrażeniach logicznych omówione zostanie tutaj — wcześniej omówienie takie nie miałoby racji bytu z uwagi na konieczność poprzedzenia go omówieniem instrukcji warunkowego wykonania kodu . Bez możliwości wybiórczego wykonywania kodu trudno zaprezentować realistyczne przykłady zastosowania operatorów logicznych. W języku HLA operator iloczynu logicznego ma postać znaków . Jest to operator binarny i jako taki wymaga określenia dwóch operandów; operandy te muszą być poprawnymi wyrażeniami czasu wykonania. Operator ten zwraca wartość logiczną „prawda” wtedy, kiedy oba operandy mają wartość „prawda”. Oto przykład: %& !"::#$$'# 9& !2 ,!'3 9&$$2#'3 (%3 Obie instrukcje zostaną wykonane jedynie wtedy, kiedy równocześnie rejestr EAX będzie miał wartość większą od 0, a rejestr CH będzie zawierał znak „a”. Jeśli którykolwiek z tych warunków nie będzie spełniony, obie instrukcje zostaną pominięte. Warto pamiętać, że operandy operatora mogą być dowolnymi poprawnymi wyrażeniami logicznymi języka HLA — nie muszą to być wyłącznie operatory relacji. Oto kilka poprawnych logicznych z operatorem iloczynu logicznego: ::" $$$ $:: ,! ,;:: ! W języku HLA wykorzystywana jest metoda skróconego szacowania wartości wyrażeń operandów operatora logicznego . Otóż jeśli lewy operand ma wartość „fałsz”, to operand prawy nie jest już sprawdzany — wyrażenie logiczne natychmiast otrzymuje wartość „fałsz” (co jest jak najbardziej poprawne, bo operator daje wartość „prawda” wyłącznie dla dwu operandów o wartości „prawda”). Stąd w ostatnim z zaprezentowanych wyżej wyrażeń najpierw sprawdzona zostanie wartość logiczna zmiennej ; jeśli będzie to wartość , test wartości logicznej rejestru EAX zostanie pominięty. 36 Asembler. Sztuka programowania Nie sposób nie zauważyć, że wyrażenie z operatorem , np. , jest wyrażeniem logicznym (zwraca albo wartość logiczną „prawda”, albo wartość „fałsz”), więc wyrażenie takie może występować w roli operandu innego wyrażenia z operatorem logicznym. Stąd poprawność następującego wyrażenia: !":: ,! !:: ! Operator iloczynu logicznego cechuje się łącznością lewostronną, co oznacza, że w kodzie wygenerowanym przez kompilator HLA ewaluacja (wartościowanie) wyrażenia następuje od strony lewej do prawej. Jeśli rejestr EAX zawiera wartość większą od zera, pozostałe operandy nie będą sprawdzane. Analogicznie, jeśli EAX jest mniejszy od zera, ale EBX jest równy EAX, to sprawdzanie logicznej wartości wyrażenia ! zostanie pominięte — całe wyrażenie od razu otrzyma wartość . Operator sumy logicznej w języku HLA reprezentowany jest znakami "". Podobnie, jak operator iloczynu, operator sumy logicznej wymaga określenia dwóch operandów będących poprawnymi wyrażeniami logicznymi. Operator zwraca przy tym wartość logiczną „prawda”, jeśli którykolwiek z operandów ma wartość logiczną „prawda”. Również podobnie jak w przypadku operatora iloczynu stosowana jest metoda skróconej ewaluacji wyrażenia — jeśli już pierwszy operand ma wartość logiczną „prawda”, operator natychmiast zwraca wartość „prawda”, niezależnie od wartości drugiego operandu. Oto przykłady zastosowania operatora sumy logicznej: <<" $$$ $<< ,! ,;<< ! Podobnie jak operator operator logicznej sumy cechuje się łącznością lewostronną, więc w wyrażeniach z wieloma operatorami "" ewaluacja przebiega od strony lewej do prawej. Na przykład: !"<< ,! !<< ! Jeśli powyższe wyrażenie zostałoby osadzone w wyrażeniu logicznym instrukcji , to kod po klauzuli zostałby wykonany tylko wtedy, kiedy rejestr EAX miałby wartość mniejszą od zera lub EBX nie byłby równy EAX bądź rejestr ECX zawierałby wartość zero. Jeśli już pierwszy test dałby wartość , pozostałe warunki nie byłyby sprawdzane. Gdyby pierwszy warunek miał wartość , ale drugi , to etap szacowania wartości logicznej wyrażenia ! zostałby pominięty. Test zawartości rejestru ECX nastąpiłby jedynie w obliczu niespełnienie wcześniejszych dwóch wyrażeń. Jeśli w jednym wyrażeniu logicznym występują operatory iloczynu i sumy logicznej, pierwszeństwo przed operatorem sumy ma operator iloczynu. Weźmy następujące wyrażenie: !"<< ,! !:: ! W interpretacji HLA powyższemu wyrażeniu równoważne jest wyrażenie następujące: !"<<& ,! !:: !' Jeśli rejestr EAX będzie miał wartość mniejszą od zera, reszta wyrażenia nie będzie sprawdzana — całość wyrażenia otrzyma wartość . Jeśli jednak rejestr EAX będzie Rozdział 1. ♦ Wstęp do języka asemblerowego 37 zawierał wartość większą od zera, to aby całość wyrażenia otrzymała wartość , oba operandy operatora iloczynu musiałyby mieć wartość . Podwyrażenia wykorzystujące operatory iloczynu i sumy logicznej można grupować wedle własnego uznania, otaczając je nawiasami. Oto przykładowe wyrażenie: & !"<< ,! !':: ! Aby to wyrażenie otrzymało wartość logiczną „prawda”, rejestr ECX musi zawierać wartość zero oraz albo wartość EAX musi być mniejsza od zera, albo rejestr EBX musi zawierać wartość różną od rejestru EAX. Szacowanie wyrażenia przebiega więc odmiennie niż w wersji bez nawiasów. Operator negacji logicznej ma w języku HLA postać znaku wykrzyknika ( ). Operandami operatora negacji mogą być jednak wyłącznie rejestry i zmienne w pamięci (np. ) — operator negacji nie może być bezpośrednio aplikowany do złożonych wyrażeń logicznych. Aby wykonać logiczną negację wyrażenia logicznego, należy to wyrażenie ująć w nawiasy i cały nawias opatrzyć przedrostkiem w postaci wykrzyknika: & !"' Powyższe wyrażenie otrzymuje wartość wtedy, kiedy rejestr EAX ma wartość mniejszą od zera. Operator logicznej negacji jest szczególnie użyteczny w odniesieniu do wyrażeń złożonych angażujących operatory logicznej sumy i logicznego iloczynu. W prostych wyrażeniach, jak powyższe, lepiej jest zazwyczaj bezpośrednio określić stan logiczny wyrażenia niż dodatkowo komplikować go operatorem negacji. W języku HLA programista może też korzystać z operatorów bitowych " i , ale te różnią się znacząco od operatorów logicznych "" oraz . Szczegółowe omówienie operatorów bitowych i innych operatorów języka HLA znajduje się w dokumentacji kompilatora. 1.9.4. Instrukcja while Składnię instrukcji ilustruje rysunek 1.11. Rysunek 1.11. Składnia instrukcji while języka HLA Wykonanie instrukcji przebiega następująco: najpierw szacowana jest wartość logiczna wyrażenia warunku pętli. Jeśli ma ono wartość „fałsz”, sterowanie natychmiast przekazywane jest do kodu znajdującego się za klauzulą . W przypadku 38 Asembler. Sztuka programowania wartości „prawda” wyrażenia warunku, procesor przystępuje do wykonania ciała pętli. Po wykonaniu wszystkich instrukcji ciała pętli następuje ponowne oszacowanie wartości logicznej wyrażenia warunkowego. Cała procedura powtarzana jest aż do momentu, w którym wyrażenie warunkowe pętli przyjmie wartość logiczną „fałsz”. W pętli sprawdzenie wartości warunku wykonania pętli następuje podobnie jak w językach wysokiego poziomu, czyli przed wykonaniem pierwszej instrukcji ciała pętli. Z tego względu istnieje prawdopodobieństwo, że instrukcje ciała pętli nie zostaną wykonane ani razu, jeśli wyrażenie warunku będzie od początku miało wartość . Poza tym warto pamiętać, że przynajmniej jedna z instrukcji ciała pętli powinna modyfikować wartość wyrażenia logicznego — w przeciwnym przypadku będziemy mieli do czynienia z pętlą nieskończoną. Oto przykład pętli w języku HLA: 9&"2'3 .# &"'( ()*)&++22'3 ((&2'3 (.# 3 1.9.5. Instrukcja for W języku HLA pętla for przybiera następującą postać: %&33 '( (%3 Instrukcja pętli odpowiada następującej pętli : 3 .# &'( 3 (.# 3 Wyrażenie inicjalizujące może być dowolną, pojedynczą instrukcją maszynową. Instrukcja ta zwykle inicjalizuje rejestr albo zmienną przechowywaną w pamięci, której wartość decyduje o kontynuowaniu pętli (tzw. licznik pętli). Warunek to wyrażenie logiczne języka HLA w formacie takim jak dla instrukcji . Wartość tego wyrażenia decyduje o podjęciu kolejnej iteracji pętli i zwykle angażuje licznik pętli. Instrukcja licznika to typowo instrukcja zwiększająca licznik pętli; odpowiednikiem tej instrukcji jest instrukcja w pętli , której wykonanie zmienia wartość logiczną warunku wykonania pętli. Zwykle instrukcją tą jest instrukcja języka HLA, na przykład . Rozdział 1. ♦ Wstęp do języka asemblerowego 39 Oto przykład ilustrujący równoważność pętli i : %&9&"2'3"3((&2''( ()*)&++22'3 (%3 // Analogiczna pętla for: 9&"2'3 .# &"'( ()*)&++22'3 ((&2'3 (.# 3 1.9.6. Instrukcja repeat Instrukcja języka HLA wykorzystuje składnię prezentowaną na rysunku 1.12. Programiści języków C, C++ oraz Java powinni od razu zauważyć, że instrukcja ta jest odpowiednikiem dostępnej w tych językach instrukcji . Rysunek 1.12. Składnia instrukcji repeat..until języka HLA Test warunku zatrzymania pętli odbywa się po zakończeniu wykonywania ciała pętli. Z tego względu instrukcja (bądź blok instrukcji) ciała pętli zostanie wykonana przynajmniej raz, niezależnie od wartości początkowej wyrażenia warunku zatrzymania pętli. Warunek ten jest badany po raz pierwszy dopiero po zakończeniu pierwszej iteracji; jeśli wyrażenie ma wartość , pętla jest powtarzana2. Jeśli warunek otrzyma wartość , pętla jest zatrzymywana i sterowanie przekazywane jest do instrukcji znajdujących się za klauzulą . Oto prosty przykład wykorzystania pętli : 9&"2 !'3 * ()*)&+ !+2 !2'3 ),&2 !'3 )& !"'3 2 Znaczenie warunku jest więc w języku HLA odwrotne niż warunku w analogicznej konstrukcji (.# znanej z języków C, C++ i Java. 40 Asembler. Sztuka programowania Jeśli instrukcje ciała pętli mają być wykonane przynajmniej raz, niezależnie od wartości wyrażenia warunku pętli, wtedy w miejsce pętli lepiej zastosować pętlę # — konstrukcja taka będzie efektywniejsza. 1.9.7. Instrukcje break oraz breakif Instrukcje $ i $ służą do przedwczesnego przekazywania sterowania poza pętlę. Składnię obu instrukcji ilustruje rysunek 1.13. Rysunek 1.13. Składnia instrukcji break oraz breakif języka HLA Instrukcja $ powoduje bezwarunkowe przerwanie wykonywania pętli; instrukcja $ uzależnia przekazanie sterowania poza pętlę od spełnienia warunku będącego wyrażeniem logicznym. Wyjście z pętli następuje wtedy wyłącznie w obliczu wartości tego wyrażenia. Zastosowanie instrukcji $ oraz $ nie pozwala na przekazanie sterowania poza pętle zagnieżdżone. W języku HLA służą do tego osobne instrukcje, jak % oraz i . Szczegółowe informacje na ten temat zawiera dokumentacja języka HLA. Dalej, język HLA udostępnia instrukcje ! oraz ! pozwalające na przystąpienie do kolejnej iteracji pętli z pominięciem niewykonanych jeszcze instrukcji ciała pętli. Po szczegóły odsyłam Czytelników do stosownej dokumentacji. 1.9.8. Instrukcja forever Składnię konstrukcji prezentuje rysunek 1.14. Rysunek 1.14. Składnia instrukcji forever języka HLA Instrukcja tworzy pętlę nieskończoną. Wewnątrz takiej pętli można jednak umieścić instrukcje $ bądź $ celem przerwania pętli w wybranym momencie. To chyba najczęstszy układ pętli . Ilustruje go następujący przykład: % ()*)&+6*.( , 4.&9 8 ("'-+'3 (= &'3 , 4%&"'3 ()*)&+6*.( .9,79 8 ("+2'3 (%3 Rozdział 1. ♦ Wstęp do języka asemblerowego 41 1.9.9. Instrukcje try, exception oraz endtry Instrukcje &!& służą do implementacji bardzo przydatnych bloków obsługi wyjątków. Składnia instrukcji prezentowana jest na rysunku 1.15. Rysunek 1.15. Składnia instrukcji obsługi wyjątków Blok instrukcji && służy do ochrony wykonania instrukcji. Jeśli wykonanie instrukcji znajdujących się pomiędzy klauzulą & a pierwszą klauzulą !, tworzących tzw. blok chroniony, przebiegnie bez niespodzianek, sterowanie przekazywane jest do pierwszej instrukcji za klauzulą &. Jeśli jednak w czasie wykonywania instrukcji bloku chronionego zdarzy się błąd (wyjątek), wykonanie programu jest przerywane, a sterowanie przekazywane jest do odpowiedniej klauzuli ! (skojarzonej z wyjątkiem, który wystąpił). Każdy wyjątek ma unikalny identyfikator w postaci liczby całkowitej. Niektóre z identyfikatorów wyjątków zadeklarowane zostały w pliku nagłówkowym excepts.hhf biblioteki standardowej języka HLA, ale programista może też definiować własne wyjątki. W momencie sprowokowania wyjątku system porównuje identyfikator zgłoszonego wyjątku z identyfikatorami określonymi w klauzulach !. Jeśli wśród nich znajduje odpowiedni identyfikator, następuje wykonanie instrukcji określonych w ramach klauzuli !. Po zakończeniu wykonywania tych instrukcji sterowanie przekazywane jest do pierwszej instrukcji za klauzulą &. Jeśli wyjątek zostanie sprowokowany poza blokiem kodu chronionego (tzn. poza ramami instrukcji &&) albo jeśli w ramach instrukcji && brak jest klauzuli skojarzonej ze zgłoszonym wyjątkiem, program jest awaryjnie zatrzymywany. Zatrzymaniu towarzyszy wyprowadzenie stosownego komunikatu na wyjście programu. Instrukcje && można na przykład wykorzystać do ochrony programu przed skutkami wprowadzenia niepoprawnych danych: 42 Asembler. Sztuka programowania * 9&% 2>( = '3// Uwaga: zmienna GoodInteger musi być typu boolean 7 ()*)&+6*.( , 4.-+'3 (= &'3 9&) 2>( = '3 ! *& !? ' ()*)&+/ **..2.*.( 8 +2'3 ! *& !;) @)@%A= ' ()*)&+6* 4 )2.*.( 8 +2'3 (73 )&>( = '3 Zastosowana w powyższym programie pętla pozwala na wykonywanie kodu ciała pętli tak długo, jak długo użytkownik podaje niepoprawne wartości. Jeśli z powodu wprowadzenia niepoprawnej wartości jej przypisanie do zmiennej '(% będzie niemożliwe (na przykład z racji umieszczenia we wprowadzonym niedozwolonych dla liczb znaków albo wprowadzenia zbyt wielkiej wartości), wykonany zostanie odpowiedni do rodzaju błędu blok instrukcji obsługi wyjątku. Obsługa wyjątków polega tu na wyświetleniu stosownego komunikatu i przekazaniu sterowania poza instrukcję &&. Ponieważ z racji wyjątku nie udało się w ramach bloku chronionego ustawić wartości dla zmiennej '(%, pętla jest wykonywana ponownie. Jeśliby jedna w ramach bloku chronionego sprowokowany został inny niż obsługiwane wyjątek, program zakończyłby działanie po uprzednim wyprowadzeniu komunikatu o błędzie3. W tabeli 1.4 zebrane zostały wyjątki, zdefiniowane w pliku excepts.hhf w czasie przygotowywania wydania niniejszej książki. Aktualną listę zdefiniowanych w bibliotece standardowej wyjątków można znaleźć, zaglądając do pliku excepts.hhf dołączonego do posiadanej wersji oprogramowania kompilatora HLA. Większość z tych wyjątków dotyczy sytuacji, których omawianie w niniejszym rozdziale byłoby przedwczesne. Zostały umieszczone w tabeli wyłącznie gwoli kompletności omówienia. Każdy z wyjątków opisany jest szczegółowo w dokumentacji HLA Reference Manual dokumentacji biblioteki standardowej języka HLA; ostateczną wykładnią jest zaś sam kod źródłowy biblioteki standardowej HLA. Najczęściej wykorzystywanymi wyjątkami są )*, ++ ,% oraz -%+ . Omówienie bloku kodu chronionego podjęte zostanie ponownie nieco później. Czytelnik powinien kontynuować edukację w zakresie podstawowym, zadowalając się na razie szczątkowymi informacjami o obsłudze wyjątków. 3 Doświadczony programista zastanawia się zapewne, dlaczego w powyższym kodzie do sterowania przebiegiem pętli wykorzystywana jest zmienna typu , — przecież można by identyczny efekt osiągnąć, stosując instrukcję , 4%. Zastosowana konstrukcja ma uzasadnienie, które zostanie przedstawione w dalszej części książki. Rozdział 1. ♦ Wstęp do języka asemblerowego 43 Tabela 1.4. Wyjątki zdefiniowane w pliku nagłówkowym excepts.hhf Identyfikator wyjątku Znaczenie !=@ %. Próba zapisania zbyt długiego łańcucha !=( ! Próba odwołania do znaku o indeksie spoza łańcucha !;) @)@%A= Zbyt wielka wartość dla danego typu zmiennej ! =?# Zaangażowanie w operacji znaku spoza dopuszczalnego zakresu (kod ASCI spoza zakresu od 0 do 127) !? Niedozwolone znaki w konwersji łańcucha do wartości numerycznej !(B C( Odwołanie do pliku za pośrednictwem niepoprawnego uchwytu pliku !B @* B) Niemożność otwarcia pliku (np. brak pliku o zadanej nazwie) !B ? Niemożność zamknięcia pliku !B 6 Błąd przy zapisie danych do pliku !B A ( Błąd przy odczycie danych z pliku !D4B) Próba zapisania danych do pliku przy pełnym dysku !(@%B Próba odczytania bajta spoza końca pliku !E 97B) Brak pamięci do zaspokojenia żądania alokacji ! 9*FD %/011 Próba odwołania do pamięci za pośrednictwem wskaźnika o wartości NULL !?B E 97 Błąd przy operacji zwalniania pamięci !6(#F= Zły format konwersji wartości liczbowej do łańcucha !FE7?9(159 Wiersz wywołania programu zawiera zbyt dużą liczbę argumentów !7#* ; Próba operowania na dwóch tablicach o różnych rozmiarach !7)( Próba odwołania do elementu spoza tablicy !(D Operacja na niepoprawnej wartości daty !(D B9 Błąd konwersji łańcucha do daty (łańcuch zawiera niedozwolone znaki) !F9 @ %. Przepełnienie w operacji manipulowania informacją o czasie ! B ( Błąd instrukcji asercji !! ) (, Próba wywołania metody klasy abstrakcyjnej ! ; Próba odwołania się do niedozwolonego adresu pamięci ! 45 Program dotarł do punktu zatrzymania (przerwanie 3.) != * Program wykonywany jest z ustawionym znacznikiem wykonania krokowego !5 Próba wykonania instrukcji charakterystycznej dla trybu nadzoru ! = Próba wykonania niedozwolonej instrukcji maszynowe !)( Rejestr zawiera wartość spoza zakresu określonego w instrukcji ,)( ! Wykonanie instrukcji przy ustawionym znaczniku przepełnienia !D( Próba dzielenia przez zero bądź inny błąd dzielenia !%D 9 Wyjątek operacji zmiennoprzecinkowej (patrz rozdział poświęcony obliczeniom arytmetycznym) !%D7G Wyjątek operacji zmiennoprzecinkowej (patrz rozdział poświęcony obliczeniom arytmetycznym) 44 Asembler. Sztuka programowania Tabela 1.4. Wyjątki zdefiniowane w pliku nagłówkowym excepts.hhf — ciąg dalszy Identyfikator wyjątku Znaczenie !% !A ) Wyjątek operacji zmiennoprzecinkowej (patrz rozdział poświęcony obliczeniom arytmetycznym) !%(@* Wyjątek operacji zmiennoprzecinkowej (patrz rozdział poświęcony obliczeniom arytmetycznym) !%@ %. Wyjątek operacji zmiennoprzecinkowej (patrz rozdział poświęcony obliczeniom arytmetycznym) !%4?# 4 Wyjątek operacji zmiennoprzecinkowej (patrz rozdział poświęcony obliczeniom arytmetycznym) !%0( %. Wyjątek operacji zmiennoprzecinkowej (patrz rozdział poświęcony obliczeniom arytmetycznym) !(C( System operacyjny odrzucił niepoprawny uchwyt !4@ %. System operacyjny zgłosił przepełnienie stosu 1.10. Biblioteka standardowa języka HLA — wprowadzenie Język HLA prezentuje się na tle tradycyjnego języka asemblerowego korzystnie dzięki dwóm aspektom. Pierwszym jest składnia języka HLA zapożyczająca elementy znane z języków programowania wysokiego poziomu (jak struktury kontroli wykonania kodu czy deklaracje zmiennych). Dostępność tych elementów pozwala na efektywne wykorzystanie umiejętności przeniesionych z programowania wysokopoziomowego i tym samym znacznie szybszą naukę języka asemblerowego. Drugim elementem stanowiącym o przewadze HLA nad tradycyjnym asemblerem jest dostępność biblioteki HLA Standard Library. Biblioteka standardowa języka HLA zawiera wiele prostych w użyciu procedur języka asemblerowego realizujących typowe zadania programistyczne — ich dostępność eliminuje konieczność samodzielnego pisania wszystkich, nawet tych najbardziej typowych, elementów programu. Dostępność biblioteki standardowej znosi jedną z podstawowych trudności, w obliczu których stają programiści chcący poznać język asemblerowy — trudność tkwiącą w konieczności samodzielnej implementacji złożonych niekiedy procedur wejścia-wyjścia, niezbędnych w nawet najprostszych programach. Brak standaryzowanej biblioteki wywołań powodowałby, że każdy początkujący programista poświęcałby mnóstwo czasu na zaimplementowanie tak podstawowych operacji jak wyprowadzanie napisów. Jej dostępność oznacza więc, że adept języka asemblerowego może skoncentrować się na nauce samego języka, nie poświęcając się od samego początku zgłębianiu tajników podsystemu wejścia-wyjścia danego systemu operacyjnego. Dostępność rozmaitych procedur bibliotecznych to nie jedyna zaleta języka HLA. W końcu biblioteki kodu asemblerowego to nie nowy wynalazek4. Jednak biblioteka standardowa 4 Popularna jest na przykład biblioteka UCR Standard Library dla programistów języka asemblerowego procesorów 80x86. Rozdział 1. ♦ Wstęp do języka asemblerowego 45 języka HLA to nie tylko sam kod procedur, ale również wysokopoziomowy interfejs wywołań tych procedur. W rzeczy samej język HLA powstał głównie z myślą o możliwości tworzenia zestawów procedur bibliotecznych wysokiego poziomu. Ów wysokopoziomowy interfejs wywołań, w połączeniu z wysokopoziomową naturą wielu procedur, daje programiście niespodziewanie wielkie i równocześnie łatwo dostępne możliwości. Biblioteka standardowa języka HLA składa się z szeregu modułów przypisanych do kilku kategorii. Część z dostępnych modułów zawiera tabela 1.55. Tabela 1.5. Wybrane moduły biblioteki standardowej HLA 5 Nazwa modułu Funkcje args Procedury przetwarzania argumentów wiersza polecenia wraz z procedurami pomocniczymi conv Procedury konwersji łańcuchów do wartości różnych typów i konwersji odwrotnych cset Procedury operujące na zbiorach znaków DateTime Procedury manipulowania datą i czasem excepts Procedury obsługi wyjątków fileio Procedury plikowych operacji wejścia-wyjścia hla Specjalne stałe języka HLA i inne definicje hll Implementacja instrukcji znanych z języków wysokopoziomowych Linux Wywołania systemowe jądra systemu Linux (tylko w wersji HLA dla systemu Linux) math Procedury implementujące obliczenia arytmetyczne o zwiększonej precyzji, funkcje trygonometrycznych i inne funkcje matematyczne memory Procedury przydziału i zwalniania pamięci wraz z procedurami pomocniczymi misctypes Definicje rozmaitych typów danych i operacji na tych typach patterns Biblioteka procedur dopasowywania wzorców rand Implementacja generatora liczb pseudolosowych i procedur pomocniczych stdin Procedury obsługi wejścia stdout Procedury obsługi wyjścia stdlib Specjalny plik nagłówkowy grupujący wszystkie moduły biblioteki standardowej języka HLA strings Biblioteka procedur manipulowania łańcuchami znakowymi tables Biblioteka procedur obsługi tablic asocjacyjnych win32 Definicje stałych wykorzystywanych w wywołaniach systemu Windows (tylko w wersji HLA dla środowiska Win32) x86 Stałe i definicje charakterystyczne dla procesora 80x86 Z racji ciągłego rozwoju biblioteki standardowej języka HLA niniejsza lista nie jest zapewne kompletna. Aktualnej listy modułów należy szukać w dokumentacji języka HLA. 46 Asembler. Sztuka programowania Część z tych modułów doczeka się szczegółowego omówienia w dalszej części książki. Tutaj skupimy się na najważniejszych — z punktu widzenia początkującego programisty — procedurach biblioteki standardowej HLA, zebranych w module . 1.10.1. Stałe predefiniowane w module stdio Omówienie modułu biblioteki standardowej należałoby rozpocząć od przedstawienia najczęściej wykorzystywanych stałych. Na przykład w następującym kodzie: ()*)&+#82* 7=(+2'3 pojawia się stała reprezentująca sekwencję znaków przejścia do nowego wiersza. Identyfikator nie jest w kontekście programu HLA żadnym słowem zarezerwowanym języka HLA; jego zastosowanie nie ogranicza się też do wywołania . Otóż to jedynie stała, której wartością jest literał łańcuchowy odpowiadający sekwencji znaków nowego wiersza (w systemie Windows sekwencja ta obejmuje znaki powrotu karetki i wysuwu wiersza; w Linuksie to pojedynczy znak wysuwu wiersza). Oprócz stałej implementacja podsystemu wejścia-wyjścia w bibliotece standardowej HLA zawiera definicje kilku innych przydatnych stałych znakowych. Zostały one wymienione w tabeli 1.6. Tabela 1.6. Stałe znakowe definiowane w module wejścia-wyjścia biblioteki standardowej HLA Stała Definicja (, Znak dzwonka; wydrukowanie tego znaku objawia się wygenerowaniem dźwięku przez głośniczek systemowy (, Znak cofania kursora (, Znak tabulacji (% Znak wysuwu wiersza ( Znak powrotu karetki Z wyjątkiem wszystkie zaprezentowane tu stałe zdefiniowane są w przestrzeni nazw (stąd wymóg poprzedzania nazw stałych przedrostkiem )6. Osadzenie owych stałych w przestrzeni nazw pozwoliło na uniknięcie ewentualnych konfliktów z nazwami zmiennych definiowanymi przez programistę. Nazwa , jako używana wyjątkowo często, nie została osadzona w przestrzeni nazw — konieczność każdorazowego wpisywania byłaby bardzo uciążliwa. 1.10.2. Standardowe wejście i wyjście programu Wiele procedur wejścia-wyjścia biblioteki HLA jest prefiksowanych nazwą bądź . Technicznie oznacza to, że nazwy tych procedur zdefiniowane zostały wewnątrz odpowiedniej przestrzeni nazw. W praktyce zastosowanie przedrostka stanowi również sugestię co do pochodzenia danych (dane pobierane ze standardowego urządzenia 6 Przestrzenie nazw omówione zostaną w jednym z kolejnych rozdziałów. Rozdział 1. ♦ Wstęp do języka asemblerowego 47 wejściowego, ) bądź ich przeznaczenia (dane kierowane do standardowego urządzenia wyjściowego, ). Najczęściej urządzeniem wejściowym jest klawiatura konsoli systemowej. Urządzenie wyjściowe to zwykle ekran konsoli. W ogólności więc instrukcje z przedrostkiem bądź realizują odczyty i zapisy danych z i do urządzenia konsoli. Przy uruchamianiu programu z poziomu wiersza poleceń (zwanego w Linuksie powłoką) można dokonać przekierowania strumieni danych wejściowych i wyjściowych do urządzeń innych niż domyślne. Na przykład argument wywołania programu w postaci $.& powoduje przekierowanie standardowego wyjścia programu do pliku plik_wy. Z kolei argument wywołania postaci $. powoduje przypisanie standardowego wejścia programu do pliku źródłowego plik_we. Oto przykłady przekierowania standardowego wejścia i wyjścia programu % uruchamianego z poziomu wiersza poleceń7: *=9*)( *=9)*)! *=9!)! 1.10.3. Procedura stdout.newln Procedura powoduje zapisanie do urządzenia standardowego wyjścia sekwencji nowego wiersza. Wywołanie owej procedury jest równoważne wywołaniu /01. Wywołanie jest jednak niekiedy nieco wygodniejsze bądź bardziej czytelne. Oto przykład wywołania procedury: () .&'3 1.10.4. Procedury stdout.putiN Procedury biblioteczne 2, 3 oraz 4 wyprowadzają na standardowe wyjście programu pojedynczą wartość, odpowiednio: 8-bitową, 16-bitową i 32-bitową, interpretowaną jako liczba całkowita ze znakiem. Argumentem wywołania może być stała, rejestr bądź zmienna w pamięci — rozmiar argumentu musi jednak odpowiadać rozmiarowi parametru formalnego procedury. Omawiane procedury wyprowadzają przekazaną wartość na standardowe wyjście programu. Wyprowadzany napis reprezentujący wartość konstruowany jest z użyciem minimalnej liczby znaków pozwalających na dokładne odwzorowanie wartości. Jeśli przekazana argumentem wywołania procedury wartość jest ujemna, napis zostanie poprzedzony znakiem minusa. Oto kilka przykładów wywołań procedur : ()*)&'3 ()*)H&(!'3 ()*)&;'3 7 Uwaga dla użytkowników systemu Linux: w zależności od wartości zmiennej środowiskowej 5FC nazwę programu trzeba być może poprzedzić znakami „./”, np. I *=9*)(. 48 Asembler. Sztuka programowania 1.10.5. Procedury stdout.putiNSize Procedury 2-, 3- oraz 4- zapisują do standardowego urządzenia wyjściowego wartości interpretowane jako liczby całkowite ze znakiem — a więc działają podobnie jak procedury . Tyle że procedury - pozwalają lepiej kontrolować format wyprowadzanych napisów — programista może odpowiednim argumentem wywołania procedury określić minimalną szerokość wyprowadzanego napisu. W przypadku kiedy wyprowadzana wartość po konwersji będzie zajmowała mniejszą niż podano liczbę znaków, brakujące znaki uzupełniane są znakami wypełnienia — znak wypełnienia również określa się argumentem wywołania procedury. Składnia wywołania procedur - jest następująca: ()*) & 2 2'3 ()*)H &!" 2 2'3 ()*) &#$ 2 2'3 W miejsce parametru można przekazać stałą, nazwę rejestru albo nazwę zmiennej w pamięci — podany argument musi jednak odpowiadać rozmiarem rozmiarowi parametru. Parametr może przyjmować argumenty w postaci stałych całkowitych z zakresu od –256 do 256. Argumentem tego parametru może być również zawartość rejestru bądź wartość zmiennej przechowywanej w pamięci. Argumentem parametru powinien być pojedynczy znak. Podobnie jak procedury procedury - wyprowadzają na urządzenie standardowego wyjścia napis reprezentujący przekazaną w wywołaniu wartość liczbową interpretowaną jako liczbę całkowitą ze znakiem. W tej wersji programista może jednak określić szerokość napisu — określa ją jako minimalną liczbę znaków, jaką powinien zajmować napis. Jeśli wyświetlana wartość zajmować będzie więcej znaków (na przykład przy próbie wyświetlenia wartości 1234 w polu o szerokości 2), to wyprowadzony napis zostanie stosownie poszerzony. Jednak w przypadku, gdy liczba znaków w napisie jest mniejsza od założonego minimum, brakujące znaki są w kodzie procedury uzupełniane znakami wypełnienia. Kiedy wyprowadzana wartość argumentu jest liczbą ujemną, napis zostanie wyrównany w ramach wyprowadzanego pola do lewej; wartość dodatnia powoduje wyrównywanie napisów do prawej. Jeśli wartość bezwzględna parametru jest większa niż minimalna liczba znaków potrzebnych do dokładnego reprezentowania wyprowadzanej wartości, procedury - uzupełniają napis znakami wypełnienia, dodając je z lewej bądź z prawej strony napisu (przed bądź za znakową reprezentacją liczby). Znak wypełnienia definiowany jest argumentem . W zdecydowanej większości przypadków znakiem tym jest znak spacji, jednak w sytuacjach szczególnych konieczne może okazać się wypełnienie innym znakiem. Należy tylko pamiętać, że argument parametru powinien być wartością znakową. W języku HLA stałe znakowe ujmowane są w znaki pojedynczego cudzysłowu. W miejsce tego parametru można też wskazać ośmiobitowy rejestr. Listing 1.4 prezentuje prosty program w języku HLA demonstrujący sposób wykorzystania procedury 4- w celu wyświetlenia listy wartości w formie tabelarycznej. Rozdział 1. ♦ Wstęp do języka asemblerowego 49 Listing 1.4. Wyprowadzenie kolumn wartości liczbowych — zastosowanie procedury stdout.puti32Size *=9/)9?)93 J)( &+(,##%+' -3 ??-3 , =/)9?)93 9&KH2'3 9&"2??'3 .# &"'( %&??'# () .&'3 9&"2??'3 (%3 ()*) &22$$'3 ),&2'3 ((&2??'3 (.# 3 () .&'3 (/)9?)93 1.10.6. Procedura stdout.put Procedura 8 to najelastyczniejsza procedura wyprowadzania napisów na wyjście programu dostępna w module obsługi standardowego wyjścia. Procedura ta łączy funkcje wszystkich pozostałych procedur wyjścia standardowego, udostępniając programiście elegancki i efektywny mechanizm wyprowadzania danych. W najbardziej ogólnej postaci składnia wywołania procedury prezentuje się następująco: ()*)& %'3 Lista argumentów wywołania procedury może zostać konstruowana ze stałych, rejestrów i zmiennych; kolejne argumenty oddziela się przecinkami. Procedura wyprowadza na standardowe wyjście wartości wszystkich kolejnych argumentów wywołania. Jako że procedura ta była już wielokrotnie prezentowana w przykładach, Czytelnik orientuje się, przynajmniej w zakresie podstawowym, co do sposobu jej wywoływania. 8 Tak naprawdę ()*) to makrodefinicja, a nie procedura. Rozróżnienie pomiędzy makrodefinicją a procedurą wykracza jednak poza zakres niniejszego rozdziału; zostanie ono uwzględnione w dalszej części książki. 50 Asembler. Sztuka programowania Warto jedynie podkreślić, że niniejsza procedura udostępnia programiście szereg właściwości, które nie były dotąd prezentowane w przykładach. W szczególności każdy z argumentów wywołania może być zadany w jednej z dwóch postaci: - może być dowolną stałą, rejestrem albo zmienną przechowywaną w pamięci. W niniejszym rozdziale prezentowane były argumenty w postaci literałów łańcuchowych i zmiennych w pamięci. Jak dotychczas wszystkie argumenty wywołania odpowiadały pierwszej możliwej postaci argumentu. W postaci drugiej programista może po dwukropku określić minimalną szerokość napisu reprezentującego wartość — szerokość ta interpretowana jest podobnie jak w procedurach -9. Przykładowy program z listingu 1.5 generuje wydruk podobny do tego tworzonego programem 5(), tyle że tutaj miejsce procedury - zajęła procedura . Listing 1.5. Demonstracja sposobu określenia szerokości napisów w argumentach procedury stdout.put *=9/)9?)93 J)( &+(,##%+' -3 ??-3 , =/)9?)93 9&KH2'3 9&"2??'3 .# &"'( %&??'# () .&'3 9&"2??'3 (%3 ()*)&-'3 ),&2'3 ((&2??'3 (.# 3 () .&'3 (/)9?)93 Procedura obsługuje jednak więcej atrybutów. Będą one omawiane w kolejnych rozdziałach, w miarę potrzeby ich wprowadzania do programów przykładowych. 9 W odróżnieniu od procedur stdout.putiNSize nie da się tu określić dowolnego znaku wypełnienia — znakiem tym jest domyślnie znak spacji. W obliczu konieczności zastosowania innego wypełnienia należy skorzystać z procedur stdout.putiNSize. Rozdział 1. ♦ Wstęp do języka asemblerowego 51 1.10.7. Procedura stdin.getc Procedura %! wczytuje pojedynczy znak z bufora urządzenia standardowego wejścia10. Znak ten umieszczany jest po wyjściu z procedury w rejestrze AL. Zastosowanie procedury ilustruje program z listingu 1.6. Listing 1.6. Demonstracja działania procedury stdin.getc *=9#*)3 J)( &+(,#%%+' ) -3 , =#*)3 // Poniższa pętla wykonywana jest dopóty, // dopóki użytkownik potwierdza wykonanie kolejnej iteracji. * // Wyprowadź 14 wartości. 9&L2) '3 .# &) "'( ()*)&) -'3 ),&2) '3 (.# 3 // Zaczekaj, aż użytkownik naciśnie 't' albo 'n'. ()*)&22+6744 8 8 &I'M-+'3 % ( (1&'3 (= &'3 , 4%&$$'3 , 4%&$$'3 ()*)&+5 .*.( ,$$2,$$-+'3 (%3 () .&'3 )&$$'3 (#*)3 10 Bufor to w tym kontekście ładniejsza nazwa tablicy. 52 Asembler. Sztuka programowania W powyższym programie wywołanie procedury 6 służy do wymuszenia przejścia do nowego wiersza strumienia wejściowego. Szczegółowy opis tej procedury znajduje się nieco dalej, w niniejszym rozdziale. 1.10.8. Procedury stdin.getiN Procedury %2, %3 oraz %4 realizują operację wczytania z urządzenia standardowego wejścia odpowiednio: 8-bitowej, 16-bitowej i 32-bitowej wartości liczbowej całkowitej. Wczytane wartości umieszczane są w rejestrach AL, AX bądź EAX. Niniejsze procedury stanowią standardowy sposób wczytywania liczb całkowitych ze znakiem do programów języka HLA. Podobnie jak procedura %!, procedury % wczytują ze standardowego wejścia sekwencję znaków. W sekwencji tej ignorowane są wszelkie początkowe znaki odstępów (spacje, znaki tabulacji i tak dalej); reszta sekwencji (zawierająca cyfry poprzedzone opcjonalnym znakiem minusa) konwertowana jest do postaci liczbowej. W przypadku, kiedy sekwencja wejściowa zawiera znaki inne niż dozwolone lub wprowadzona wartość nie mieści się w zakresie liczbowym charakterystycznym dla danej procedury, zgłaszany jest wyjątek (można go przechwycić, umieszczając wywołanie procedury w bloku kodu chronionego w instrukcji &&). Procedura %2 obsługuje liczby z zakresu od –128 do 127, %3 — od –32 768 do 32 767; wartości wczytywane do programu za pośrednictwem procedury %4 muszą zaś mieścić się w przedziale od –2 147 483 648 do 2 147 483 647. Sposób wykorzystania owych procedur ilustruje listing 1.7. Listing 1.7. Przykład wykorzystania wywołań stdin.getiN *=9*)3 J)( &+(,##%+' -3 H-H3 -3 , =*)3 // Pobierz od użytkownika kilka liczb całkowitych różnej wielkości. ()*)&+6*.( . 4 , 4.& 4 )N'-+'3 (= &'3 9&2'3 ()*)&+6*.( . 4 , 4.& 4 )NHNHN'-+ '3 (= H&'3 9&!2H'3 ()*)&+6*.( , 4.& 4 )((9(.'-+'3 (= &'3 Rozdział 1. ♦ Wstęp do języka asemblerowego 53 9& !2'3 // Wyświetl podane wartości. ()*) & 2 +@.*.( ,7-+222 +,. ,4.-+2-22 +H,. ,4.-+2H-22 +,. ,4.-+2-2 '3 (*)3 Warto skompilować i uruchomić powyższy program i sprawdzić, co się stanie, jeśli wprowadzane liczby będą wykraczały poza dozwolony zakres albo ciągi wartości zawierać będą niedozwolone znaki. 1.10.9. Procedury stdin.readLn i stdin.flushInput Moment wywołania procedury %! czy %4 nie musi być tożsamy z momentem wprowadzenia danych przez użytkownika. Biblioteka standardowa języka HLA buforuje dane wprowadzane na standardowe wejście, wczytując jednorazowo cały wiersz tekstu wprowadzanego na to wejście. Wywołanie procedury wejścia może więc zostać zrealizowane za pośrednictwem operacji odczytu danych z bufora wejściowego (o ile ten nie jest pusty). Choć buforowanie w ogólności poprawia efektywność manipulowania danymi wejściowymi, niekiedy wprowadza zamieszanie, jak w poniższym przykładzie: ()*)&+6*.( . 4 , 4.& 4 )N'-+'3 (= &'3 9&2'3 ()*)&+6*.( . 4 , 4.& 4 )NHNHN'-+'3 (= H&'3 9&!2H'3 Programista ma nadzieję, że wykonanie powyższego kodu da następujący efekt: program wyświetli monit o podanie liczby, zaczeka na wprowadzenie jej przez użytkownika, wyświetli kolejny monit i również zaczeka na wprowadzenie liczby. W rzeczywistości program może mieć nieco inny przebieg. Jeśli, na przykład, powyższy kod zostanie uruchomiony, a użytkownik w odpowiedzi na pierwszy monit wprowadzi łańcuch „123 456” program nie będzie już po wyświetleniu drugiego monitu oczekiwał na wprowadzenie danych — procedura %3 odczyta po prostu drugi z wprowadzonych do bufora wejściowego łańcuchów (456). Procedury wejścia oczekują na wprowadzenie tekstu przez użytkownika jedynie wtedy, kiedy bufor wejściowy jest pusty. Dopóki zawiera on jakiekolwiek znaki, procedury wejścia ograniczają się do odczytu z bufora. Można to wykorzystać, konstruując następujący kod: 54 Asembler. Sztuka programowania ()*)&+6*.( (. ,74. -+'3 (= &'3 9& !2'3 (= &'3 9& !2# ;'3 Tutaj zezwala się wprost użytkownikowi na jednorazowe (w ramach pojedynczego wiersza) wprowadzenie dwóch liczb. Liczby te powinny zostać oddzielone jednym bądź kilkoma znakami odstępu. Pozwala to na zaoszczędzenie miejsca na ekranie. Tutaj buforowanie danych wejściowych okazało się korzystne. Kiedy indziej jednak może być odwrotnie. Na szczęście biblioteka standardowa języka HLA przewiduje dwie procedury sterujące działaniem bufora standardowego urządzenia wejściowego. Są to procedury 6 oraz (. Pierwsza z nich usuwa z bufora wszystko, co zostało w nim umieszczone wcześniej, i tym samym wymusza wprowadzenie przez użytkownika nowego wiersza danych. Druga po prostu usuwa całą zawartość bufora. Oczyszczenie bufora powoduje, że przy następnym wywołaniu dowolnej z procedur wejścia, użytkownik będzie musiał wprowadzić nowy wiersz danych. Procedurę 6 wywołuje się zwykle bezpośrednio przez wywołaniem dowolnej procedury wejścia. Procedura ( wywoływana jest zaś typowo bezpośrednio po odczytaniu danych z bufora wybraną procedurą wejścia. Jeśli w programie wykorzystywana jest procedura 6 i okaże się, że konieczne jest dwukrotne wprowadzenie danych, to należy zastanowić się nad wykorzystaniem w jej miejsce procedury (. W ogólności ta ostatnia procedura, pozwalająca na opróżnienie bufora przez kolejną operacją wejścia, jest wykorzystywana znacznie częściej. Konieczność zastosowania procedury 6 jest bardzo rzadka — należy ją stosować wyłącznie tam, gdzie konieczne jest wymuszenie na użytkowniku wprowadzenia nowego, aktualnego wiersza danych. 1.10.10. Procedura stdin.get Procedura % łączy w sobie przedstawione wcześniej procedury wejścia, udostępniając ich funkcje za pośrednictwem pojedynczego wywołania. Procedura % jest przy tym nieco prostsza w użyciu niż , ponieważ jedynymi argumentami wywołania tej pierwszej są nazwy zmiennych (lub rejestrów), w których mają zostać umieszczone wczytane wartości. Przyjrzyjmy się ponownie przykładowi z poprzedniego punktu: ()*)&+6*.( (. ,74. -+'3 (= &'3 9& !2'3 (= &'3 9& !2# ;'3 Równoważny przykład wykorzystujący procedurę % wyglądałby następująco: ()*)&+6*.( (. ,74. -+'3 (= &2# ;'3 Rozdział 1. ♦ Wstęp do języka asemblerowego 55 Jak widać, zastosowanie procedury % może program skrócić i zwiększyć jego czytelność. Należy przy tym pamiętać, że wywołanie procedury % powoduje umieszczenie wczytanych wartości od razu we wskazanych zmiennych; żadne z wczytanych wartości nie pojawią się w rejestrach, chyba że rejestry te wystąpią na liście argumentów wywołania. Argumentami wywołania mogą być zarówno nazwy zmiennych, jak i nazwy rejestrów. 1.11. Jeszcze o ochronie wykonania kodu w bloku try..endtry Jak zapewne Czytelnik pamięta, instrukcje && otaczają blok kodu, którego wykonanie może potencjalnie sprowokować wyjątki zakłócające działanie programu. Wyjątki mogą mieć trzy źródła: mogą być zgłaszane sprzętowo (np. w przypadku dzielenia przez zero), generowane przez system operacyjny albo podnoszone wykonaniem odpowiedniej instrukcji języka HLA. Programista może przechwytywać i obsługiwać sytuacje wyjątkowe, umieszczając stosowny kod w ramach klauzuli !. Typowy przykład zastosowania instrukcji && ilustruje listing 1.8. Listing 1.8. Przykład zastosowania bloku try..endtry *=9 (*)3 J)( &+(,##%+'3 )-)H3 , = (*)3 7 ()*)&+6*.( , 4., 4)-+'3 (= &)'3 ()*)&+6*.( -+2)2'3 ! *& !? ' ()*)&+6*.( 7= . ( . 4+'3 ! *& !;) @)@%A= ' ()*)&+6*.( ,8 ,7() +'3 (73 ( (*)3 56 Asembler. Sztuka programowania W języku HLA instrukcje znajdujące się za słowem &, a przed pierwszą klauzulą ! noszą miano instrukcji chronionych. Jeśli podczas wykonywania takich instrukcji zgłoszony zostanie wyjątek, sterowanie zostanie przekazane do pierwszej klauzuli ! skojarzonej z danym wyjątkiem, przeszukując kolejne klauzule pod kątem zgodności identyfikatora wyjątku zadeklarowanego ze zgłoszonym11. Identyfikator wyjątku to po prostu 32-bitowa liczba. Stąd również każda wartość umieszczana w nawiasach klauzuli ! powinna być wartością 32-bitową. Predefiniowane wartości wyjątków języka HLA wymienione są w pliku excepts.hhf. I choć byłoby to niewątpliwie rażącym naruszeniem przyjętego powszechnie stylu programowania, w klauzulach ! dozwolone jest podawanie wprost wartości numerycznych wyjątków bez korzystania z predefiniowanych nazw stałych. 1.11.1. Zagnieżdżone bloki try..endtry Jeśli w wyniku przeszukania klauzul ! nie nastąpi dopasowanie identyfikatora wyjątku, będzie miało miejsce przeszukanie klauzul nadrzędnego bloku &&, w którym blok bieżący jest zagnieżdżony dynamicznie. Spójrzmy na przykład z listingu 1.9. Listing 1.9. Zagnieżdżanie instrukcji try..endtry *=9 (*)3 J)( &+(,##%+'3 )-)H3 , = (*)3 7 7 ()*)&+6*.( , 4., 4)-+'3 (= &)'3 ()*)&+6*.( -+2)2'3 ! *& !? ' ()*)&+6*.( 7= . ( . 4+'3 (73 ()*)&+( .74 * * 4 )+'3 ! *& !;) @)@%A= ' 11 W programie HLA identyfikator ten umieszczany jest w rejestrze EAX. Stąd po przekazaniu sterowania do odpowiedniej klauzuli ! * można odwoływać się do identyfikatora wyjątku, odwołując się do rejestru EAX. Rozdział 1. ♦ Wstęp do języka asemblerowego 57 ()*)&+6*.( ,8 ,7() +'3 (73 ( (*)3 Na listingu 1.9 widać zagnieżdżone dwa bloki instrukcji &&. Jeśli w ramach wykonania instrukcji % użytkownik wprowadzi wartość większą od 4 miliardów z kawałkiem, procedura % podniesie wyjątek ++ ,%. Kiedy wyjątek ten zostanie przekazany do systemu wykonawczego HLA, nastąpi przeszukanie klauzul ! bieżącego bloku && (tego, w którym nastąpiło zgłoszenie wyjątku; w prezentowanym przykładzie jest to blok zagnieżdżony). Jeśli nie uda się dopasować identyfikatora wyjątku ++ ,% w bieżącym bloku &&, system wykonawczy HLA sprawdzi, czy bieżący blok && nie został zagnieżdżony w innym bloku && (co akurat ma miejsce na listingu 1.9). W takim przypadku system wykonawczy HLA przeszuka również klauzule ! bloku zewnętrznego (nadrzędnego). Na listingu 1.9 ów blok zawiera klauzulę z odpowiednią wartością identyfikatora, więc sterowanie przekazywane jest do pierwszej instrukcji umieszczonej za klauzulą !/++ ,%0. Po opuszczeniu bloku && system wykonawczy traktuje ów blok jako nieaktywny i jeśli teraz nastąpi zgłoszenie wyjątku, system wykonawczy HLA nie będzie przeszukiwał klauzul ! tego bloku12. Pozwala to na różnicowanie procedur obsługi wyjątków w różnych miejscach programu. Jeśli ten sam wyjątek obsługują dwa bloki &&, a jeden z tych bloków jest zagnieżdżony w bloku chronionym pierwszego, to w momencie zgłoszenia wyjątku podczas wykonywania wewnętrznego bloku chronionego system wykonawczy HLA przekaże sterowanie do kodu obsługi wyjątku w wewnętrznym bloku &&. Kod obsługi tego samego wyjątku w zewnętrznym bloku jest ignorowany — HLA nie przekazuje sterowania do tego kodu. W przykładzie z listingu 1.9 druga instrukcja && została statycznie zagnieżdżona w pierwszym bloku &&13. Wcześniej wspomniano, że jeśli bieżący blok && nie zawiera kodu obsługi zgłoszonego wyjątku, następuje przeszukanie klauzul ! kolejnych bloków &&, w których blok bieżący został zagnieżdżony dynamicznie. W przypadku zagnieżdżenia dynamicznego zagnieżdżenie nie wynika wprost ze struktury kodu źródłowego. Sterowanie może zostać przekazane z wnętrza bloku && do zupełnie innego miejsca programu, niżby to wynikało z samej struktury kodu. Jeśli w tamtym miejscu realizowany jest blok chroniony instrukcji &&, to mamy do czynienia z zagnieżdżeniem dynamicznym. Zagnieżdżenie dynamiczne można zrealizować na kilka sposobów, a najbardziej chyba intuicyjnym (i zapewne znanym Czytelnikowi z języków wysokiego poziomu) z nich jest wywołanie 12 13 Chyba że sterowanie zostanie wcześniej przekazane z powrotem do bloku 7 (7, na przykład kiedy jest on umieszczony w ciele pętli. Zagnieżdżenie statyczne należy rozumieć jako „fizyczne”, bo wynikające wprost ze struktury kodu źródłowego, umieszczenie jednego bloku wewnątrz drugiego. Kiedy mowa o zagnieżdżaniu instrukcji, zwykle chodzi o zagnieżdżanie statyczne. 58 Asembler. Sztuka programowania procedury. Kiedy więc w dalszej części książki omawiany będzie sposób pisania i wywoływania procedur w języku asemblerowym, należy pamiętać, że wywołanie procedury z bloku kodu chronionego może doprowadzić do dynamicznego zagnieżdżenia bloków && — wystarczy, aby procedura również zawierała blok && (i aby został on wykonany). 1.11.2. Klauzula unprotected bloku try..endtry Wykonywanie instrukcji & powoduje zawsze zachowanie bieżącego środowiska wyjątków i przygotowanie systemu wykonawczego (na wypadek zgłoszenia wyjątku) do przekazania sterowania do odpowiedniej klauzuli !. Jeśli program przebrnie przez blok instrukcji chronionych, pierwotne środowisko wyjątków jest przywracane, a sterowanie przekazywane jest do pierwszej instrukcji znajdującej się za klauzulą &. Bardzo ważną rolę w tej procedurze ma ostatni jej etap, czyli przywrócenie pierwotnego środowiska wyjątków. W przypadku jego pominięcia wszelkie kolejne wyjątki powodowałyby przekazanie sterowania do opuszczonego już bloku &&. Problem ten ilustruje listing.1.10. Listing 1.10. Niepoprawne opuszczenie bloku instrukcji try..endtry *=9 (*)L3 J)( &+(,##%+' *)-)3 , = (*)L3 // Poniższa pętla jest kontynuowana dopóty, dopóki użytkownik wprowadza // nieprawidłową liczbę; wyjście z pętli następuje w wyniku wykonania instrukcji break. % 7 ()*)&+5(8 , 4.-+'3 (= &*)'3 ()*)&+6*.( -+2*)2'3 , 43 ! *& !;) @)@%A= ' ()*)&+1 ,8 ,7() 3*. .*.( +'3 ! *& !? ' ()*)&+?= . ( . 43*. .*.( + '3 (73 (%3 Rozdział 1. ♦ Wstęp do języka asemblerowego 59 // Uwaga: poniższy kod znajduje się poza pętlą i nie jest osadzony w bloku chronionym // instrukcji try..endtry ()*)&+5(88 8 ( , -+'3 (= &*)'3 ()*)&+/. ,-+2*)2'3 ( (*)L3 Niniejszy przykład z pozoru implementuje niezawodny, odporny na błędy system wprowadzania danych: wokół bloku kodu chronionego rozciągnięta jest pętla wymuszająca ponowne wprowadzenie danych przez użytkownika, jeśli w poprzedniej próbie nie udało się ich poprawnie odczytać. Pomysł jest dobry; problem tkwi w jego nieprawidłowej realizacji. Otóż instrukcja $ powoduje opuszczenie pętli bez przywrócenia poprzedniego stanu środowiska wyjątków. Z tego względu, kiedy program wykonuje procedurę % umieszczoną poza pętlą (a tym bardziej poza blokiem kodu chronionego), system obsługi wyjątków wciąż traktuje jej kod jako chroniony. Jeśli więc przy okazji jego wykonania zgłoszony zostanie wyjątek, sterowanie zostanie przekazane z powrotem do stosownej klauzuli ! opuszczonego już przecież bloku &&. Jeśli identyfikatorem wyjątku będzie ++ ,% albo )*, wyświetlony zostanie stosowny komunikat, a użytkownik ponownie zmuszony będzie do wprowadzenia liczby. Tego zaś programista raczej nie przewidywał. Przekazanie sterowania do nieodpowiedniego bloku && to tylko część problemu. Kod z listingu 1.10 obarczony jest inną poważną wadą, związaną ze sposobem zachowywania i następnie przywracania przez system wykonawczy HLA środowiska wyjątków. W szczególności bowiem HLA zachowuje informacje o stanie systemu wyjątków w specjalnym obszarze pamięci zwanym stosem. Jeśli po wyjściu z bloku && stan środowiska wyjątków nie zostanie przywrócony, informacje o tym środowisku pozostaną na stosie; obecność na nim nadmiarowych danych może zaś doprowadzić do błędnego działania programu. Jest już chyba jasne, że program nie powinien opuszczać bloku && z pominięciem etapu przywrócenia pierwotnego stanu środowiska wyjątków. Wiadomo, że sposób wprowadzania danych zaprezentowany na listingu 1.10 jest niepoprawny, ale z drugiej strony sama koncepcja wymuszania powtarzania operacji wprowadzania aż do skutku jest jak najbardziej słuszna. Można ją zaimplementować przy użyciu specjalnej klauzuli bloku &&. Spójrzmy na kod z listingu 1.11. Listing 1.11. Blok pozbawiony ochrony wewnątrz bloku try..endtry *=9 (*)3 J)( &+(,##%+' *)-)3 , = (*)3 60 Asembler. Sztuka programowania // Poniższa nieskończona pętla wykonywana jest dopóty, dopóki użytkownik wprowadza // niepoprawne dane. Wyjście z pętli następuje w wyniku wykonania instrukcji break. // Instrukcja ta, mimo że obecna wewnątrz bloku try..endtry, znajduje się w bloku // kodu niechronionego sygnalizowanego klauzulą unprotected. % 7 ()*)&+5(8 , 4.-+'3 (= &*)'3 ()*)&+6*.( -+2*)2'3 )* ( , 43 ! *& !;) @)@%A= ' ()*)&+1 ,8 ,7() 3*. .*.( +'3 ! *& !? ' ()*)&+?= . ( . 43*. .*.( + '3 (73 (%3 // Uwaga: poniższy kod znajduje się poza pętlą i nie jest osadzony w bloku chronionym // instrukcji try..endtry ()*)&+5(88 8 ( , -+'3 (= &*)'3 ()*)&+/. ,-+2*)2'3 ( (*)3 Osiągnięcie klauzuli ! inicjuje operację przywrócenia pierwotnego stanu środowiska wyjątków. Oczywiste jest, że po przejściu do bloku kodu niechronionego wykonanie kodu nie jest już chronione na wypadek zgłoszenia wyjątków. Klauzula ! nie znosi jednak ochrony kodu realizowanej w dynamicznie zagnieżdżonych zewnętrznych blokach && — klauzula ! odnosi się jedynie do tego bloku &&, w którym została umieszczona. Jako że na listingu 1.11 instrukcja przerywająca pętlę, $, znajduje się za klauzulą !, przekazanie sterowania poza pętlę odbywa się po uprzednim przywróceniu stanu wyjątków. Słowo !, jeśli już występuje w ramach instrukcji &, powinno znajdować się bezpośrednio za blokiem kodu chronionego. Musi więc poprzedzać wszystkie klauzule !. Rozdział 1. ♦ Wstęp do języka asemblerowego 61 W obliczu zgłoszenia wyjątku system wykonawczy HLA automatycznie przywraca pierwotny stan środowiska wykonania. Z tego względu w ramach kodu obsługi wyjątku, po klauzuli ! można swobodnie korzystać z instrukcji $, nie obawiając się o negatywne efekty charakterystyczne dla niepoprawnego opuszczenia bloku &&. Z racji przywrócenia pierwotnego stanu wyjątków w bloku niechronionym oraz w kodzie obsługi wyjątku po klauzuli !, wystąpienie wyjątku w jednym z tych obszarów powoduje natychmiastowe przekazanie sterowania do zewnętrznego bloku &&, w którym blok bieżący został dynamicznie zagnieżdżony. W przypadku braku takiego bloku program jest awaryjnie przerywany, a na standardowe wyjście wyprowadzany jest stosowny komunikat o błędzie. 1.11.3. Klauzula anyexception bloku try..endtry Typowo instrukcji & towarzyszy szereg klauzul ! obsługujących wszelkie wyjątki, których wystąpienie w kodzie chronionym zdoła przewidzieć programista. Niekiedy to nie wystarcza — konieczne jest wtedy zapewnienie obsługi wszystkich możliwych (a nie tylko przewidzianych) wyjątków, aby program nie został przedwcześnie zakończony. Jeśli programista jest autorem całości kodu bloku chronionego, powinien być zdolny do przewidzenia wszystkich sytuacji wyjątkowych związanych potencjalnie z wykonaniem bloku. Jeśli jednak w bloku tym znajdują się wywołanie procedury bibliotecznej, wywołanie funkcji interfejsu systemu operacyjnego czy inne instrukcje, nie w pełni kontrolowane przez programistę, przewidzenie wszystkich możliwych wyjątków może być niemożliwe. Tymczasem zgłoszenie wyjątku o identyfikatorze innym niż identyfikatory podane w kolejnych klauzulach ! może doprowadzić do załamania programu. Szczęśliwie język HLA przewiduje umieszczenie w bloku && klauzuli &!, do której dopasowywane są wszystkie wyjątki, których identyfikatorów nie uda się odpasować do klauzul !. Klauzula &! nie różni się wiele od klauzuli ! — jedyna odmienność tkwi w braku konieczności określania identyfikatora wyjątku (to oczywiste). Jeśli obok klauzul ! w bloku && znajduje się klauzula &!, powinna ona być ostatnią klauzulą obsługi wyjątków w bloku. Dopuszczalne jest naturalnie, aby klauzula &! była jedyną klauzulą obsługi wyjątków w bloku &&. Po przekazaniu sterowania do klauzuli &! identyfikator wyjątku umieszczany jest w rejestrze EAX. Identyfikator ten można w ramach kodu obsługi wyjątku sprawdzać, dopasowując obsługę do przyczyny zgłoszenia wyjątku. 1.11.4. Instrukcja try..endtry i rejestry Każdorazowe wkroczenie do bloku && oznacza konieczność zachowania na stosie 16 bajtów informacji o stanie środowiska wyjątków. Przywrócenie środowiska wyjątków po opuszczeniu tego bloku (albo w wyniku osiągnięcia klauzuli !) wymaga odczytania właśnie owych 16 bajtów. Do momentu zgłoszenia wyjątku realizowanie kodu chronionego nie wpływa na zawartość żadnych rejestrów. Sytuacja zmienia się w momencie zgłoszenia wyjątku w wyniku wykonania instrukcji kodu chronionego. 62 Asembler. Sztuka programowania Po przejściu do klauzuli ! rejestr EAX zawiera identyfikator (numer) wyjątku. Reszta rejestrów ogólnego przeznaczenia zawiera wartości nieokreślone. Ponieważ system operacyjny może zgłosić wyjątek w reakcji na błąd sprzętowy, nie powinno się nawet zakładać, że rejestry ogólnego przeznaczenia zawierać będą po rozpoczęciu obsługi wyjątku wartości, które znajdowały się w nich pierwotnie. Kod generowany przez system HLA do obsługi wyjątków może być różny w różnych wersjach kompilatora, więc poleganie na zawartości rejestrów w kodzie obsługi wyjątków jest co najmniej ryzykowne. Jako że po przejściu do bloku kodu obsługi wyjątku nie można czynić żadnych założeń co do zawartości rejestrów (z wyjątkiem rejestru EAX), to jeśli kod kontynuowany za klauzulą & zakłada obecność w rejestrach jakichś konkretnych wartości (np. wartości umieszczonych tam przed wkroczeniem do bloku kodu chronionego), należy te wartości przywrócić samodzielnie. Zaniedbanie tego może doprowadzić do błędnego działania programu, przy czym błędy tego rodzaju są tym trudniejsze do wykrycia, że sytuacje wyjątkowe z definicji zdarzają się rzadko, co utrudnia odtworzenie i diagnostykę błędu; dodatkowym utrudnieniem jest to, że nie zawsze zgłoszenie wyjątku musi zmienić wartość konkretnego rejestru. Sposób rozwiązania problemu zachowania wartości rejestrów prezentuje następujący fragment kodu: )9-3 O 9&"2)9'3 %&9&"2 ,!'3 ,!3& ,!''( *)#& ,!'3// Zachowanie wartości rejestru EBX na wypadek wyjątku. % 7 (= &'3 )* (, 43 ! *& !? ' ()*)&+/ ( ..PQ3*,)8*. -+'3 (73 (%3 **& ,!'3// Przywrócenie zawartości rejestru EBX. ((& ,!2 !'3 ((& !2)9'3 (%3 Ponieważ mechanizm obsługi wyjątków powoduje potencjalne zmiany wartości rejestrów i ponieważ obsługa wyjątków to proces stosunkowo nieefektywny, nie powinno się stosować instrukcji & w roli zwykłej struktury sterującej wykonaniem programu (na przykład symulując w bloku && działanie instrukcji wyboru znanych z języków wysokiego poziomu, jak ! czy !). Takie praktyki wpływają ujemnie na wydajność programu; mogą tez wprowadzać niepożądane, trudne do wykrycia i zdiagnozowania efekty uboczne wynikające z zakłócania zawartości rejestrów. Rozdział 1. ♦ Wstęp do języka asemblerowego 63 Działanie mechanizmu wyjątków opiera się na założeniu, że rejestr EBP wykorzystywany jest wyłącznie w roli wskaźnika rekordów aktywacji (rekordy aktywacji omawiane są w rozdziale poświęconym procedurom). Standardowo programy HLA wykorzystują ów rejestr właśnie w tej roli. Warunkiem poprawnego działania programów jest więc unikanie modyfikowania zawartości rejestru EBP. Jeśli rejestr ten zostanie użyty w roli rejestru ogólnego przeznaczenia, na przykład w obliczeniach arytmetycznych, obsługa wyjątków systemu wykonawczego HLA nie będzie działać poprawnie, pojawić się też mogą dodatkowe problemy. To samo tyczy się zresztą rejestru ESP — jego również nie należy wykorzystywać jako rejestru ogólnego przeznaczenia.