Linux Assembly x86-32 Architettura Programmazione
Transcript
Linux Assembly x86-32 Architettura Programmazione
Linux Assembly x86-32 Architettura e Programmazione (Vol.1) Claudio Daffra [email protected] http://linuxassemblyx86.125mb.com/index.html GPL - 2006 INDICE GENERALE CAPITOLO 1 – Architettura X86 – a pag 30 Definizioni CISC / RISC / CRISP Unità di Elaborazione Centrale BUS CHIPSET I/O Memoria ROM/RAM Memoria Cache Wait States / Locality reference Interrupt Comunicazione tra circuiti Funzionamento BUS dati / indirizzi / controllo Clock di sistema Segmenti e memoria 80x ... Registri Stack Flag Il linguaggio assembly Big Little Endian Rappresentation Il set di istruzioni x86 Estensioni MMX Estensioni SSE Estensioni SSE2 CAPITOLO 2 – I sistemi numerici – a pag 54 Sistema decimale Sistema binario Floating point IEEE 754 Conversioni Numeri negativi Sistema esadecimale Organizzazione numerica Estensione da una classe ad un'altra Binary Coded Decimal BCD Codice ASCII TABELLA ASCII CAPITOLO 3 – Pronti VIA ! - a pag 77 Pronti Via! – il primo programma Compilatore GAS PUSH / POP Come funzionano le librerie condivise secondo programma INT CALL RET Compilatore NASM CAPITOLO 4 - Aritmetica e Logica 1 - a pag 93 Rappresentazione numerica Operazioni Logiche AND OR NOT XOR Istruzioni Macchina logiche Test Btx Operazioni di rotazione SHR / SHL / SAL / SAR ROL / ROR / RCL / RCR Modi di indirizzamento CAPITOLO 5 – Aritmetica e Logica 2 – a pag 115 Addizione / Sottrazione Moltiplicazione / Divisione Incremento / Decremento BITSCAN La prima libreria di macro Commento al nono programma Somma e sottrazione a 64 bit Addizione e Sottrazione perchè limitarsi ? CAPITOLO 6 – Tutto sui FLAG ! (o quasi) - a pag 138 Flag CLC / STC / CMC Salti Condizionati CLD / STD CLI / STI SETcc PUSHF / POPF PUSHA / POPA SAHF / LAHF CICLI MOV Condizionali RDTSC Il timer software CAPITOLO 7 – Aritmetica e logica 3 – a pag 157 IDIV / IMUL Un problema di conversione CBW / CWD / CDQ / CWDE MOVSX / MOVZX SHLD / SHRD SWAP! Qualche istruzione di aggiustamento! Convenzioni di chiamata Variabili locali alle funzioni ENTER / LEAVE C e Funzioni CAPITOLO 8 – Stringhe e scarpe ! - a pag 175 Stringhe e scarpe Da minuscolo a MAIUSCOLO CMP STRSET / STRLEN SCAS / STOS REP Dividiamo tutto ! Linkare Libreria ASM al C Curiosità! LEA MOVS / CMPS / LODS CAPITOLO 9 – Impariamo il C ! - a pag 195 Variabili Puntatori FOR ... NEXT NREAK / CONTINUE / EXIT IF ... THEN ... ELSE ... ENDIF IF ... AND ... OR ... THEN IF ... OR ... NOT WHILE / ENDWHILE REPEAT / UNTIL LOOP / ENDLOOP SWITCH Espressioni Booleane State Machine Dulcis in fundu ! CAPITOLO 10 – Non Ricordo ! - a pag 222 Ho bisogno di memoria Strutture e Array Sherlock Holmes ! Array Bidimensionali Array Multidimensionali Funzioni ricorsive Variabili Statiche Tutto è una bugia! Passaggio di parametri CAPITOLO - 11 l'x87 – a pag 243 Programmiamo l'x87 Formati Interi Formati Reali Suffissi Caricare dalla memoria Operazioni di confronto La parola di stato Registro della parola di stato Registro della parola di controllo Registri di etichetta Istruzioni di controllo Miscellanea Istruzioni matematiche Distanza fra due punti CAPITOLO 12 – Iniziamo Con l'assembly – a pag 259 Hello World ! Syscall Miglioriamo l'output Caratteri terminale GOTOXY Visualizziamo a colori INPUT La libreria NCURSES Apriamo una finestra sul mondo CAPITOLO 13 – L'elfo & hex – a pag 281 Ancora sulle macro Elf header SYSCALL OPEN SYSCALL READ SYSCALL WRITE SYSYCAL CLOSE Caratteri del terminale Sequenze ASCII (ESC) SYSCALL BRK make CAPITOLO 14 – MMX SSE SSE2 – a pag 300 Concetti generali MMX CPUID Trasferimento dei dati Istruzioni aritmetiche AND e XOR Istruzioni di Confronto Istruzioni di Shift SSE XMMx SSE2 CAPITOLO 15 – Ottimizzare – a pag 336 Parola D'ordine ottimizzare. La memoria Perchè il pentium è CISC Codifica istruzioni prefetch cache pipeline Ottimizzare gli array Calcolo indici array multidimensionali Errori comuni Divide et impera! Usare LEA per moltiplicare Programmazione Super scalare Previsione dei salti Pipeline in virgola mobile Ritardi CAPITOLO 16 – Sfidare il compilatore ! - a pag 359 Iniziamo READ TIMER prima sfida sfidiamo strcpy sfidiamo strncpy sfidiamo strlen strlen atto 2 strlen atto 3 ottimizzare calcoli matrici CAPITLO 17 – Grafica che passione – a pag 379 SDL primo listato in c traduzione in assembly Secondo esempio SDL INIT Assembly inline Esempi CAPITOLO 18 – Musica Maestro ! - a pag 394 Scheda sonora libsdl mixer primo listato C Codifica in Assembly CAPITOLO 19 – GNOME / GTK – a pag 421 installare gnome listato uno Hello World Elenco alcune librerie Teoria dei segnali funzioni di CallBack eventi / GDK event Esaminando in dettaglio CAPITOLO - 20 OOP – a pag 438 Object Oriented Programming Overloading Costruttori e distruttori Ereditarietà Funzioni Friend Funzioni INLINE Variabili STATIC Array di Classi Puntatori ad oggetti Puntatori a membri di una classe allocazione dinamica Try Throw Catch Funzioni Virtuali Template Classi generiche Iteratori CAPITOLO – 21 GLIBC & SYSCALL - pag 478 Glib/syscall User/kernel space Avvio/termine processi Directory FILE TIME CAPITOLO – 22 Comunicazione tra processi – a pag 497 I segnali Tipi di segnali Funzione signal SIGTERM ALLARMI Funzione sigaction FIFO Memoria condivisa CAPITOLO 23 – Networking – a pag 518 Modello Client/server Modello Peer to Peer Protocolli TCP/IP SOCKET Servizi Primo ed ultimo esempio ! CAPITOLO 24 – X86-64 – a pag 531 Introduzione Gestione della memoria Uno sguardo d'insieme Accedere alle informazioni Data movement instruction Istruzioni aritmetico/logiche Istruzioni di controllo cicli procedure stack passare gli argomenti stack frame Ancora ricorsione CAPITOLO 25 – Disassembling - a pag 547 Disassembling readelf primo esempio obj dump debug codice in runtime Prefazione Spero vivamente che il materiale da me raccolto possa per voi essere utile. Questo (mini) libro nasce dalle mia esigenza personale di raccogliere e mettere a disposizione quanto più materiale possibile relativamente all'argomento della programmazione assembly in ambiente linux. L'open source, questo movimento ha visto la creazione di montagne di documentazione tradotta in più lingue, con molto materiale didattico e software rivolto a tutti. Oggi giorno è difficile non trovare qualche HowTo su un qualche argomento; tuttavia nella mia personale ricerca, relativamente alla programmazione in assembly in linux, molto materiale era ancora in lingua inglese, ancora nel panorama italiano pochi (ma esperti) le persone che trattavano questo argomento ed ancora poco materiale in italiano. Ancora alcuni libri, validi senza ombra di dubbio prendono molto in considerazione l'aspetto teorico, forniscono delle routine precotte per facilitare l'apprendimento di alcune nozioni, e sorvolano alcuni aspetti che dal mio canto ricercavo. Nel mio caso, avevo la necessità dopo aver letto quei libri di applicare con sicurezza e concretamente le nozioni acquisite. Mi trovavo al punto di partenza, sapevo come usare una routine di stampa, volevo visualizzare un carattere a colori in una data posizione dello schermo, banale ora che conosco come farlo, ma a quel tempo lo trovavo piuttosto difficile. Quello che voglio dire è che il lettore ha l'esigenza al fine della lettura di un argomento/articolo di mettere in pratica le nozioni concretamente, direttamente in un ambiente di sviluppo e con gli strumenti a sua disposizione; questo è l'obiettivo del mio libro, fornire qualche nozione teorica, individuare un ambiente operativo e mostrare gli esempi da bash, e spiegare passo passo con il relativo output il programma che evidenzia le nozioni prima esplicate, indicando al lettore come arrivare da zero al risultato finale, anche con i comandi impartiti dalla bash. Dal primo capitolo del mio libro ho voluto improntare una dissertazione teorica/pratica basata su esempi, che via via diventano più complessi diminuendo i commenti e la spiegazione del codice, in quanto già appreso. E' un poco come parlare una seconda lingua basta parlare con gli esempi per capire. Ed in particolar modo ho cercato di interagire con il sistema operativo, cercando di individuare le esigenze individuali, e non certo delle mie personali routine. Il libro tocca molti aspetti generali, per darvi una visione d'insieme sulle possibilità offerte dalla macchina e dall'ambiente, sta poi al lettore approfondire l'argomento se di suo interesse. Ad oggi sto iniziando a scrivere linux assembly vol. 2, anche se ho a disposizione molto poco tempo, in quanto sto scrivendo parallelamente codice con Irrlicht. Tutto il materiale da me raccolto, è liberamente distribuibile modificabile, copiabile. Questo libro è distribuito con la licenza GPL. Spero anche il mio sito sia di vostro riferimento : http://linuxassemblyx86.125mb.com/index.html Tuttavia, non voglio considerare questo libro come scritto da un esperto del settore, un prof. o qualcosa d'altro di etichettabile, ma : solo un vostro amico che ha raccolto del materiale per voi. Claudio Daffra La GNU GPL è stata scritta da Richard Stallman e Eben Moglen nel 1989 la versione 1.0 e nel 1991 la versione 2.0, per distribuire i programmi creati dal Progetto GNU. È basata su una licenza simile usata per le prime versioni di GNU Emacs. Contrapponendosi alle licenze per software proprietario, la GNU GPL permette all'utente libertà di utilizzo, copia, modifica e distribuzione; a partire dalla sua creazione è diventata una delle licenze per software libero più usate. Attualmente e' in corso di definizione da parte della FSF la terza versione della GPL . Temini di licenza : Quanto segue è un riassunto dei termini della licenza. L'unica descrizione legalmente precisa, in ogni caso, è quella del testo della licenza stessa. Il testo della GNU GPL è disponibile per chiunque riceva una copia di un software coperto da questa licenza. I licenziatari (da qui in poi indicati come "utenti") che accettano le sue condizioni hanno la possibilità di modificare il software, di copiarlo e ridistribuirlo con o senza modifiche, sia gratuitamente sia a pagamento. Quest'ultimo punto distingue la GNU GPL dalle licenze che proibiscono la ridistribuzione commerciale. Se l'utente distribuisce copie del software, deve rendere disponibile il codice sorgente a ogni acquirente, incluse tutte le modifiche eventualmente effettuate (questa caratteristica è detta copyleft). Nella pratica, i programmi sotto GNU GPL vengono spesso distribuiti allegando il loro codice sorgente, anche se la licenza non lo richiede. Ci sono casi in cui viene distribuito solo il codice sorgente, lasciando all'utente il compito di compilarlo. L'utente è tenuto a rendere disponibile il codice sorgente solo alle persone che hanno ricevuto da lui la copia del programma o, in alternativa, accompagnare il software con una offerta scritta di rendere disponibile il sorgente su richiesta e per il solo costo della copia. Questo significa, ad esempio, che è possibile creare versioni private di un software sotto GNU GPL, a patto che tale versione non venga distribuita a qualcun altro. Questo accade quando l'utente crea delle modifiche private al software ma non lo distribuisce: in questo caso non è tenuto a rendere pubbliche le modifiche.Dato che il software è protetto da copyright, l'utente non ha altro diritto di modifica o ridistribuzione al di fuori dalle condizioni di copyleft. In ogni caso, l'utente deve accettare i termini della GNU GPL solo se desidera esercitare diritti normalmente non contemplati dalla legge sul copyright, come la ridistribuzione. Al contrario, se qualcuno distribuisce un software (in particolare, versioni modificate) senza rendere disponibile il codice sorgente o violando in altro modo la licenza, può essere denunciato dall'autore originale secondo le stesse leggi sul copyright. È un intelligente cavillo legale, ed è per questo che la GNU GPL è stata descritta come un "copyright hack". La licenza specifica anche che il diritto illimitato di ridistribuzione non è garantito, in quanto potrebbero essere trovate delle debolezze legali (o "bug") all'interno della definizione di copyleft. GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machinereadable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. one line to give the program's name and an idea of what it does. Copyright (C) yyyy name of author This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. Licenza GPL (In italiano) Questa è una traduzione italiana non ufficiale della Licenza Pubblica Generica GNU. Non è pubblicata dalla Free Software Foundation e non ha valore legale nell'esprimere i termini di distribuzione del software che usa la licenza GPL. Solo la versione originale in inglese della licenza ha valore legale. Ad ogni modo, speriamo che questa traduzione aiuti le persone di lingua italiana a capire meglio il significato della licenza GPL. This is an unofficial translation of the GNU General Public License into Italian. It was not published by the Free Software Foundation, and does not legally state the distribution terms for software that uses the GNU GPL--only the original English text of the GNU GPL does that. However, we hope that this translation will help Italian speakers understand the GNU GPL better. LICENZA PUBBLICA GENERICA (GPL) DEL PROGETTO GNU Versione 2, Giugno 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Traduzione curata da gruppo Pluto, da ILS e dal gruppo italiano di traduzione GNU. Ultimo aggiornamento 19 aprile 2000. Chiunque può copiare e distribuire copie letterali di questo documento di licenza, ma non ne è permessa la modifica. Preambolo Le licenze della maggior parte dei programmi hanno lo scopo di togliere all'utente la libertà di condividere e modificare il programma stesso. Viceversa, la Licenza Pubblica Generica GNU è intesa a garantire la libertà di condividere e modificare il software libero, al fine di assicurare che i programmi siano liberi per tutti i loro utenti. Questa Licenza si applica alla maggioranza dei programmi della Free Software Foundation e ad ogni altro programma i cui autori hanno deciso di usare questa Licenza. Alcuni altri programmi della Free Software Foundation sono invece coperti dalla Licenza Pubblica Generica Minore. Chiunque può usare questa Licenza per i propri programmi. Quando si parla di software libero (free software), ci si riferisce alla libertà, non al prezzo. Le nostre Licenze (la GPL e la LGPL) sono progettate per assicurarsi che ciascuno abbia la libertà di distribuire copie del software libero (e farsi pagare per questo, se vuole), che ciascuno riceva il codice sorgente o che lo possa ottenere se lo desidera, che ciascuno possa modificare il programma o usarne delle parti in nuovi programmi liberi e che ciascuno sappia di potere fare queste cose. Per proteggere i diritti dell'utente, abbiamo bisogno di creare delle restrizioni che vietino a chiunque di negare questi diritti o di chiedere di rinunciarvi. Queste restrizioni si traducono in certe responsabilità per chi distribuisce copie del software e per chi lo modifica. Per esempio, chi distribuisce copie di un programma coperto da GPL, sia gratis sia in cambio di un compenso, deve concedere ai destinatari tutti i diritti che ha ricevuto. Deve anche assicurarsi che i destinatari ricevano o possano ottenere il codice sorgente. E deve mostrar loro queste condizioni di licenza, in modo che essi conoscano i propri diritti. Proteggiamo i diritti dell'utente in due modi: (1) proteggendo il software con un copyright, e (2) offrendo una licenza che dia il permesso legale di copiare, distribuire e modificare il Programma. Inoltre, per proteggere ogni autore e noi stessi, vogliamo assicurarci che ognuno capisca che non ci sono garanzie per i programmi coperti da GPL. Se il programma viene modificato da qualcun altro e ridistribuito, vogliamo che gli acquirenti sappiano che ciò che hanno non è l'originale, in modo che ogni problema introdotto da altri non si rifletta sulla reputazione degli autori originari. Infine, ogni programma libero è costantemente minacciato dai brevetti sui programmi. Vogliamo evitare il pericolo che chi ridistribuisce un programma libero ottenga la proprietà di brevetti, rendendo in pratica il programma cosa di sua proprietà. Per prevenire questa evenienza, abbiamo chiarito che ogni brevetto debba essere concesso in licenza d'uso a chiunque, o non avere alcuna restrizione di licenza d'uso. Seguono i termini e le condizioni precisi per la copia, la distribuzione e la modifica. LICENZA PUBBLICA GENERICA GNU TERMINI E CONDIZIONI PER LA COPIA, LA DISTRIBUZIONE E LA MODIFICA 0. Questa Licenza si applica a ogni programma o altra opera che contenga una nota da parte del detentore del copyright che dica che tale opera può essere distribuita sotto i termini di questa Licenza Pubblica Generica. Il termine "Programma" nel seguito si riferisce ad ogni programma o opera così definita, e l'espressione "opera basata sul Programma" indica sia il Programma sia ogni opera considerata "derivata" in base alla legge sul copyright; in altre parole, un'opera contenente il Programma o una porzione di esso, sia letteralmente sia modificato o tradotto in un'altra lingua. Da qui in avanti, la traduzione è in ogni caso considerata una "modifica". Vengono ora elencati i diritti dei beneficiari della licenza. Attività diverse dalla copiatura, distribuzione e modifica non sono coperte da questa Licenza e sono al di fuori della sua influenza. L'atto di eseguire il Programma non viene limitato, e l'output del programma è coperto da questa Licenza solo se il suo contenuto costituisce un'opera basata sul Programma (indipendentemente dal fatto che sia stato creato eseguendo il Programma). In base alla natura del Programma il suo output può essere o meno coperto da questa Licenza. 1. È lecito copiare e distribuire copie letterali del codice sorgente del Programma così come viene ricevuto, con qualsiasi mezzo, a condizione che venga riprodotta chiaramente su ogni copia una appropriata nota di copyright e di assenza di garanzia; che si mantengano intatti tutti i riferimenti a questa Licenza e all'assenza di ogni garanzia; che si dia a ogni altro destinatario del Programma una copia di questa Licenza insieme al Programma. È possibile richiedere un pagamento per il trasferimento fisico di una copia del Programma, è anche possibile a propria discrezione richiedere un pagamento in cambio di una copertura assicurativa. 2. È lecito modificare la propria copia o copie del Programma, o parte di esso, creando perciò un'opera basata sul Programma, e copiare o distribuire tali modifiche o tale opera secondo i termini del precedente comma 1, a patto che siano soddisfatte tutte le condizioni che seguono: a) Bisogna indicare chiaramente nei file che si tratta di copie modificate e la data di ogni modifica. b) Bisogna fare in modo che ogni opera distribuita o pubblicata, che in parte o nella sua totalità derivi dal Programma o da parti di esso, sia concessa nella sua interezza in licenza gratuita ad ogni terza parte, secondo i termini di questa Licenza. c) Se normalmente il programma modificato legge comandi interattivamente quando viene eseguito, bisogna fare in modo che all'inizio dell'esecuzione interattiva usuale, esso stampi un messaggio contenente una appropriata nota di copyright e di assenza di garanzia (oppure che specifichi il tipo di garanzia che si offre). Il messaggio deve inoltre specificare che chiunque può ridistribuire il programma alle condizioni qui descritte e deve indicare come reperire questa Licenza. Se però il programma di partenza è interattivo ma normalmente non stampa tale messaggio, non occorre che un'opera basata sul Programma lo stampi. Questi requisiti si applicano all'opera modificata nel suo complesso. Se sussistono parti identificabili dell'opera modificata che non siano derivate dal Programma e che possono essere ragionevolmente considerate lavori indipendenti, allora questa Licenza e i suoi termini non si applicano a queste parti quando queste vengono distribuite separatamente. Se però queste parti vengono distribuite all'interno di un prodotto che è un'opera basata sul Programma, la distribuzione di quest'opera nella sua interezza deve avvenire nei termini di questa Licenza, le cui norme nei confronti di altri utenti si estendono all'opera nella sua interezza, e quindi ad ogni sua parte, chiunque ne sia l'autore. Quindi, non è nelle intenzioni di questa sezione accampare diritti, né contestare diritti su opere scritte interamente da altri; l'intento è piuttosto quello di esercitare il diritto di controllare la distribuzione di opere derivati dal Programma o che lo contengano. Inoltre, la semplice aggregazione di un'opera non derivata dal Programma col Programma o con un'opera da esso derivata su di un mezzo di memorizzazione o di distribuzione, non è sufficente a includere l'opera non derivata nell'ambito di questa Licenza. 3. È lecito copiare e distribuire il Programma (o un'opera basata su di esso, come espresso al comma 2) sotto forma di codice oggetto o eseguibile secondo i termini dei precedenti commi 1 e 2, a patto che si applichi una delle seguenti condizioni: a) Il Programma sia corredato del codice sorgente completo, in una forma leggibile da calcolatore, e tale sorgente sia fornito secondo le regole dei precedenti commi 1 e 2 su di un mezzo comunemente usato per lo scambio di programmi. b) Il Programma sia accompagnato da un'offerta scritta, valida per almeno tre anni, di fornire a chiunque ne faccia richiesta una copia completa del codice sorgente, in una forma leggibile da calcolatore, in cambio di un compenso non superiore al costo del trasferimento fisico di tale copia, che deve essere fornita secondo le regole dei precedenti commi 1 e 2 su di un mezzo comunemente usato per lo scambio di programmi. c) Il Programma sia accompagnato dalle informazioni che sono state ricevute riguardo alla possibilità di ottenere il codice sorgente. Questa alternativa è permessa solo in caso di distribuzioni non commerciali e solo se il programma è stato ottenuto sotto forma di codice oggetto o eseguibile in accordo al precedente comma B. Per "codice sorgente completo" di un'opera si intende la forma preferenziale usata per modificare un'opera. Per un programma eseguibile, "codice sorgente completo" significa tutto il codice sorgente di tutti i moduli in esso contenuti, più ogni file associato che definisca le interfacce esterne del programma, più gli script usati per controllare la compilazione e l'installazione dell'eseguibile. In ogni caso non è necessario che il codice sorgente fornito includa nulla che sia normalmente distribuito (in forma sorgente o in formato binario) con i principali componenti del sistema operativo sotto cui viene eseguito il Programma (compilatore, kernel, e così via), a meno che tali componenti accompagnino l'eseguibile. Se la distribuzione dell'eseguibile o del codice oggetto è effettuata indicando un luogo dal quale sia possibile copiarlo, permettere la copia del codice sorgente dallo stesso luogo è considerata una valida forma di distribuzione del codice sorgente, anche se copiare il sorgente è facoltativo per l'acquirente. 4. Non è lecito copiare, modificare, sublicenziare, o distribuire il Programma in modi diversi da quelli espressamente previsti da questa Licenza. Ogni tentativo di copiare, modificare, sublicenziare o distribuire il Programma non è autorizzato, e farà terminare automaticamente i diritti garantiti da questa Licenza. D'altra parte ogni acquirente che abbia ricevuto copie, o diritti, coperti da questa Licenza da parte di persone che violano la Licenza come qui indicato non vedranno invalidata la loro Licenza, purché si comportino conformemente ad essa. 5. L'acquirente non è tenuto ad accettare questa Licenza, poiché non l'ha firmata. D'altra parte nessun altro documento garantisce il permesso di modificare o distribuire il Programma o i lavori derivati da esso. Queste azioni sono proibite dalla legge per chi non accetta questa Licenza; perciò, modificando o distribuendo il Programma o un'opera basata sul programma, si indica nel fare ciò l'accettazione di questa Licenza e quindi di tutti i suoi termini e le condizioni poste sulla copia, la distribuzione e la modifica del Programma o di lavori basati su di esso. 6. Ogni volta che il Programma o un'opera basata su di esso vengono distribuiti, l'acquirente riceve automaticamente una licenza d'uso da parte del licenziatario originale. Tale licenza regola la copia, la distribuzione e la modifica del Programma secondo questi termini e queste condizioni. Non è lecito imporre restrizioni ulteriori all'acquirente nel suo esercizio dei diritti qui garantiti. Chi distribuisce programmi coperti da questa Licenza non e' comunque tenuto a imporre il rispetto di questa Licenza a terzi. 7. Se, come conseguenza del giudizio di un tribunale, o di una imputazione per la violazione di un brevetto o per ogni altra ragione (non limitatamente a questioni di brevetti), vengono imposte condizioni che contraddicono le condizioni di questa licenza, che queste condizioni siano dettate dalla corte, da accordi tra le parti o altro, queste condizioni non esimono nessuno dall'osservazione di questa Licenza. Se non è possibile distribuire un prodotto in un modo che soddisfi simultaneamente gli obblighi dettati da questa Licenza e altri obblighi pertinenti, il prodotto non può essere affatto distribuito. Per esempio, se un brevetto non permettesse a tutti quelli che lo ricevono di ridistribuire il Programma senza obbligare al pagamento di diritti, allora l'unico modo per soddisfare contemporaneamente il brevetto e questa Licenza e' di non distribuire affatto il Programma. Se una qualunque parte di questo comma è ritenuta non valida o non applicabile in una qualunque circostanza, deve comunque essere applicata l'idea espressa da questo comma; in ogni altra circostanza invece deve essere applicato questo comma nel suo complesso. Non è nelle finalità di questo comma indurre gli utenti ad infrangere alcun brevetto né ogni altra rivendicazione di diritti di proprietà, né di contestare la validità di alcuna di queste rivendicazioni; lo scopo di questo comma è unicamente quello di proteggere l'integrità del sistema di distribuzione dei programmi liberi, che viene realizzato tramite l'uso di licenze pubbliche. Molte persone hanno contribuito generosamente alla vasta gamma di programmi distribuiti attraverso questo sistema, basandosi sull'applicazione fedele di tale sistema. L'autore/donatore può decidere di sua volontà se preferisce distribuire il software avvalendosi di altri sistemi, e l'acquirente non può imporre la scelta del sistema di distribuzione. Questo comma serve a rendere il più chiaro possibile ciò che crediamo sia una conseguenza del resto di questa Licenza. 8. Se in alcuni paesi la distribuzione o l'uso del Programma sono limitati da brevetto o dall'uso di interfacce coperte da copyright, il detentore del copyright originale che pone il Programma sotto questa Licenza può aggiungere limiti geografici espliciti alla distribuzione, per escludere questi paesi dalla distribuzione stessa, in modo che il programma possa essere distribuito solo nei paesi non esclusi da questa regola. In questo caso i limiti geografici sono inclusi in questa Licenza e ne fanno parte a tutti gli effetti. 9. All'occorrenza la Free Software Foundation può pubblicare revisioni o nuove versioni di questa Licenza Pubblica Generica. Tali nuove versioni saranno simili a questa nello spirito, ma potranno differire nei dettagli al fine di coprire nuovi problemi e nuove situazioni. Ad ogni versione viene dato un numero identificativo. Se il Programma asserisce di essere coperto da una particolare versione di questa Licenza e "da ogni versione successiva", l'acquirente può scegliere se seguire le condizioni della versione specificata o di una successiva. Se il Programma non specifica quale versione di questa Licenza deve applicarsi, l'acquirente può scegliere una qualsiasi versione tra quelle pubblicate dalla Free Software Foundation. 10. Se si desidera incorporare parti del Programma in altri programmi liberi le cui condizioni di distribuzione differiscano da queste, è possibile scrivere all'autore del Programma per chiederne l'autorizzazione. Per il software il cui copyright è detenuto dalla Free Software Foundation, si scriva alla Free Software Foundation; talvolta facciamo eccezioni alle regole di questa Licenza. La nostra decisione sarà guidata da due finalità: preservare la libertà di tutti i prodotti derivati dal nostro software libero e promuovere la condivisione e il riutilizzo del software in generale. NON C'È GARANZIA 11. POICHÉ IL PROGRAMMA È CONCESSO IN USO GRATUITAMENTE, NON C'È GARANZIA PER IL PROGRAMMA, NEI LIMITI PERMESSI DALLE VIGENTI LEGGI. SE NON INDICATO DIVERSAMENTE PER ISCRITTO, IL DETENTORE DEL COPYRIGHT E LE ALTRE PARTI FORNISCONO IL PROGRAMMA "COSÌ COM'È", SENZA ALCUN TIPO DI GARANZIA, NÉ ESPLICITA NÉ IMPLICITA; CIÒ COMPRENDE, SENZA LIMITARSI A QUESTO, LA GARANZIA IMPLICITA DI COMMERCIABILITÀ E UTILIZZABILITÀ PER UN PARTICOLARE SCOPO. L'INTERO RISCHIO CONCERNENTE LA QUALITÀ E LE PRESTAZIONI DEL PROGRAMMA È DELL'ACQUIRENTE. SE IL PROGRAMMA DOVESSE RIVELARSI DIFETTOSO, L'ACQUIRENTE SI ASSUME IL COSTO DI OGNI MANUTENZIONE, RIPARAZIONE O CORREZIONE NECESSARIA. 12. NÉ IL DETENTORE DEL COPYRIGHT NÉ ALTRE PARTI CHE POSSONO MODIFICARE O RIDISTRIBUIRE IL PROGRAMMA COME PERMESSO IN QUESTA LICENZA SONO RESPONSABILI PER DANNI NEI CONFRONTI DELL'ACQUIRENTE, A MENO CHE QUESTO NON SIA RICHIESTO DALLE LEGGI VIGENTI O APPAIA IN UN ACCORDO SCRITTO. SONO INCLUSI DANNI GENERICI, SPECIALI O INCIDENTALI, COME PURE I DANNI CHE CONSEGUONO DALL'USO O DALL'IMPOSSIBILITÀ DI USARE IL PROGRAMMA; CIÒ COMPRENDE, SENZA LIMITARSI A QUESTO, LA PERDITA DI DATI, LA CORRUZIONE DEI DATI, LE PERDITE SOSTENUTE DALL'ACQUIRENTE O DA TERZI E L'INCAPACITÀ DEL PROGRAMMA A INTERAGIRE CON ALTRI PROGRAMMI, ANCHE SE IL DETENTORE O ALTRE PARTI SONO STATE AVVISATE DELLA POSSIBILITÀ DI QUESTI DANNI. FINE DEI TERMINI E DELLE CONDIZIONI Appendice: come applicare questi termini a nuovi programmi Se si sviluppa un nuovo programma e lo si vuole rendere della maggiore utilità possibile per il pubblico, la cosa migliore da fare è rendere tale programma libero, cosicché ciascuno possa ridistribuirlo e modificarlo sotto questi termini. Per fare questo, si inserisca nel programma la seguente nota. La cosa migliore da fare è mettere la nota all`inizio di ogni file sorgente, per chiarire nel modo più efficiente possibile l'assenza di garanzia; ogni file dovrebbe contenere almeno la nota di copyright e l'indicazione di dove trovare l'intera nota. <una riga per dire in breve il nome del programma e cosa fa> Copyright (C) <anno> <nome dell'autore> Questo programma è software libero; è lecito redistribuirlo o modificarlo secondo i termini della Licenza Pubblica Generica GNU come è pubblicata dalla Free Software Foundation; o la versione 2 della licenza o (a propria scelta) una versione successiva. Questo programma è distribuito nella speranza che sia utile, ma SENZA ALCUNA GARANZIA; senza neppure la garanzia implicita di NEGOZIABILITÀ o di APPLICABILITÀ PER UN PARTICOLARE SCOPO. Si veda la Licenza Pubblica Generica GNU per avere maggiori dettagli. Questo programma deve essere distribuito assieme ad una copia della Licenza Pubblica Generica GNU; in caso contrario, se ne può ottenere una scrivendo alla Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Si aggiungano anche informazioni su come si può essere contattati tramite posta elettronica e cartacea. Se il programma è interattivo, si faccia in modo che stampi una breve nota simile a questa quando viene usato interattivamente: Orcaloca versione 69, Copyright (C) anno nome dell'autore Orcaloca non ha ALCUNA GARANZIA; per dettagli usare il comando `show g'. Questo è software libero, e ognuno è libero di ridistribuirlo secondo certe condizioni; usare il comando `show c' per i dettagli. Gli ipotetici comandi "show g" e "show c" mostreranno le parti appropriate della Licenza Pubblica Generica. Chiaramente, i comandi usati possono essere chiamati diversamente da "show g" e "show c" e possono anche essere selezionati con il mouse o attraverso un menù, o comunque sia pertinente al programma. Se necessario, si deve anche far firmare al proprio datore di lavoro (per chi lavora come programmatore) o alla propria scuola, per chi è studente, una "rinuncia al copyright" per il programma. Ecco un esempio con nomi fittizi: Yoyodinamica SPA rinuncia con questo documento ad ogni diritto sul copyright del programma `Orcaloca' (che svolge dei passi di compilazione) scritto da Giovanni Smanettone. <firma di Primo Tizio>, 1 April 3000 Primo Tizio, Presidente I programmi coperti da questa Licenza Pubblica Generica non possono essere incorporati all'interno di programmi proprietari. Se il proprio programma è una libreria di funzioni, può essere più utile permettere di collegare applicazioni proprietarie alla libreria. Se si ha questa intenzione consigliamo di usare la Licenza Pubblica Generica Minore GNU (LGPL) invece di questa Licenza. CAPITOLO 1 Architettura Introduzione Questo capitolo vuole solo essere una breve introduzione sull'hardware della macchina e su alcuni concetti basilari i quali verrano ripresi, nei capitoli successivi dedicati alla programmazione. Vengono presi in considerazione solo alcuni aspetti dell'hardware che interessa prettamente la programmazione del microprocessore o altri per capirne il funzionamento, senza addentrarsi nei particolari o nelle classificazioni; Di volta in volta programmando affronterò l'aspetto hardware per una miglior comprensione dell'argomento in questione. Architettura x86 Il computer è un : elaboratore elettronico di dati, automatizzato e programmabile; perciò un computer “sa cosa fare” ma non “come fare”! Una macchina dialoga con l'uomo attraverso schemi prefissati, precisi seguendo uno svolgimento preordinato, programmato a priori, quindi in modo automatico; Tuttavia puo' essere programmato, (software) e quindi svolgere compiti diversi, definizione in contrasto con automazione; tuttavia puo' svolgere operazioni gia' programmate nei sofware; in definitiva i computer sono destinati ad eseguire un solo tipo di operazione : l'elaborazione dei dati. Spiegare come funzionano i processori non e' semplice , possiamo suddividere le architetture in 3 grosse componenti a seconda della complessita' : CISC Complex instruction Set Commputer Computer con set di istruzioni complesso, questo tipo di processori praticamente viene utilizzato da tutte le famiglie dei pc; la caratteristica da cui deriva il nome consiste nel fatto che questi processori utilizzano dei comandi interni complessi che vengono trattati ad ogni ciclo. RISC Redeuced Istruction Set Computer A differenza dei processori cisc questi lavorano con un numero di istruzioni ridotto, ci sono meno comandi, sono piu' veloci son anche piu' semplici. CRISP Complex Reduced istruction Set Computer Un set di istruzioni complesse ma ridotte introdotte con gli attuali Pentium. L'architettura attualmente utilizzata dalla maggior parte moderni computer e' quella definita da John Von Neumann. (VNA ) Si compone di 3 componenti principali : – – – l'unità di elaborazione centrale (CPU Central Processing Unit); sistemi di input /output; la memoria ; L'unita' di elaborazione centrale Il microprocessore, è : un circuito integrato. La CPU e' il cuore del computer, essa svolge tutte le operazione necessarie al corretto funzionamento del computer, viene ulteriormente suddivisa in : – UC Unità Centrale : che controlla e coordina le funzioni della CPU; interpreta i comandi provenienti dalle varie altre unità; – ALU Unità aritmetico logica : svolge tutte le operazioni comparative e logiche, matematiche; – FPU Unita' in virgola mobile (coprocessore matematico), gestisce tutte le operazioni, con i numeri in vigola mobile, (decimali); – MMU Unita' di gestione della memoria : gestisce tutte le operazioni, di indirizzamento alla memoria per conto della CPU; – PTU (protection test unit) : svolge funzioni di controllo sulla correttezza delle operazioni effettuate; – BIU (bus interface unit) : gestisce i trasferimenti tra dati e i componenti del pc. BUS Un BUS è : un collegamento tra CPU e periferiche, trasporta in entrata ed in uscita i dati tra i vari componenti del pc. I BUS hanno dimensioni diverse che possono variare dai 16 a 64 bit; questo dato è importante in quanto determina la quantità di dati che puo' essere trasferito simultaneamente. Possiamo distinguere tre categorie di bus : – BUS Dati : trasporto dati in input/output da/verso la CPU ; – BUS Indirizzi : trasporta l'indirizzo di memoria dove sono presenti i dati ; – BUS controllo : trasporta alcuni segnali di controllo ed il tipo di operazine che dovrà essere eseguita su essa (Write/Red) . CHIPSET Sono dei microcirtuiti integrati nella scheda madre o nei dispositivi I/O al fine di gestire meglio il corretto flusso dati e sincronizzazione tra CPU e periferiche : – – – – – – – – – – – – – – interfaccia del coprocessore matematico ; generatore di clock ; controllre DMA (controllo diretto alla memoria) ; PPI (interfaccia periferica programmabile) ; controller CRT (controllo per i monitor) UART (Universal Asyncronous Receiver Trasmitter) per dispositivi asincroni; controller Floppy; Controller EIDE , controllo per disco rigido, disco floppy CD- ROM DVD ; PCI bridge : RTC (clock in tempo reale) IrDA (infrared data association), collega dispositivi ad infrarossi; Controller Mouse, tastiera ; Controller cache L1, L2 SRAM CMOS , impostazione di configurazione del pc altri chipset Ali (Acer Laboratories), produce chipset per le schede madri ; SiS (Silicon Integrated System) riunisce North Bride e south Bridge ; VIA (VIA technologies). Le unità di I/O In definitiva le unità di I/O sono tutti quei componenti hardware che possono essere collegati alla CPU (Esempio : stampante, mouse, tastiera, video, Hard disc, dvd-rom). Il collegamento avviene tramite cavi che si inseriscono nella Scheda madre o tramite piedini in oro o rame. A seconda della loro funzione vengo distinte i tre categorie : – – – periferiche di INPUT periferiche di OUTPUT periferiche di I/O : : : es. tastiera, mouse, cd-rom,scanner,joystick ; es. video,stampante, ; es. hard disk, memoria,floppy,modem; Memoria ROM La memoria ROM Read Only Access Memory e' un dispositivo a sola lettura dove e' possibile solo leggere le informazioni, senza possibilita' di modificarle. In questa memoria viene memorizzato il BIOS Basic input output standard, che e' un insieme di istruzioni basi che serve per eseguire le piu' semplici operazioni in un pc, e quindi caricare un programma (bootstrap) il quale a sua volta carica il Sistema Operativo. Viene generalmente copiato nell'ultimo segmento di memoria del primo megabyte ( 0FF00h 0FFFFh ). In alcuni computer PALMTOP vengono incisi anche nella memoria alcuni programmi applicativi. Questo tipo di memoria e' non volatile, cio sisgnifica che non si cancella quando viene tolta l'alimentazione al computer. Memoria RAM Una memoria e' un dispositivo elettronico di immagazzinamento dati. . La ram e' molto piu' veloce che un disco rigido o qualsiasi altra memoria, in media l'accesso ha dai di un hard disk e' circa 80 milli/secondi, (m/s) al pari l'accesso alla ram si parla di 50 / 80 nano/secondi (n/s). tuttavia per poter memorizzare i dati queste devono essere sincronizzate con la cpu, che elabora i dati ad una velocita' notevolmente superiore, quindi per poter effettuare le operazioni in modo sincrono, la cpu deve effettuare dei WAIT STATE o tempi di attesa. Memorie molto veloci sono molto costose, all'interno della microprocessore, ci sono le memorie cache di livello 1 2 3, pochi kb che lavorano con il processore 1:1. Le azioni all'interno di un pc vengono sincronizzate buona parte con uno o piu' orologi, la velocita' di clock interna del processore scandisce il tempo in cui i segnali elettronici e i dati vengono inviati all'interno del PC. Il clock di sistema imposta la durata ed il numero di cicli elettronici disponibili in un secondo. I cicli sono il meccanismo di sincroniazzazione tra dati e istruzioni. La misura del clock viene data in MegaHertz, un hertz e' la variazione di un segnale elettronico da alto a basso o viceversa. Un mega hertz corrisponde a un milione di hertz al secondo : – – – 1 1.000.000 1.000.000.000 1 hertz 1 mega herz 1 giga hertz 1 oscillazione (alto/basso) ; 1 milione di oscillazioni ; 1 miliardo di oscillazioni . Da un punto di vista teorica un computer da 1.000 Mhz e' in grado di svolgere (1 G/hz ) e' in grado di svolgere un miliardo di operazioni al secondo. tuttavia la velocità dei processori 'e' dichiarata in MIPS (million istruction per second). indica la quantità di comandi : istruzioni elementari del processore che vengono elaborati in un secondo all'interno della cpu, RAM Random access memory viene usata nei pc come memoria principale, quando viene eseguito un programma questo viene caricato dall'hard disk ed una sua copia viene trasferita in ram, ha una velocita' di trasferimento dati molto elevata. Anche se non proprio approppiato l'accesso casuale denota il fatto che possiamo accedere ad ogni elemento della memoria direttamente e non in modo sequenziale (nastri). Sono detti dispositivi volatili, cioe' non mantengono nulla memorizzato se non opportunamente alimentati, quindi allo spegnimento del pc tutto il contenuto della momoria RAM va perso. L'unita' di misura della ram, cioe' la quantita' di dati che si puo' memorizzare viene definita in BYTE ( 8 bit ) : byte 1 1 byte 1 carattere 1 carattere kylobyte 1024 1 kilobyte un migliaio una pagina megabyte 1.048.576 1 megabyte un milione un libro gigabyte 1.073.741.824 1 gigabyte un miliardo 100 libri terabyte 1.000.000.000.000 1 terabyte una biblioteca petabyte 1.000.000.000.000.000 1 petabyte un milione di miliardi un migliaio di miliardi tutte le bibliotece usa tabella velocita' ram e bus dati 20 33 66 100 133 Mhz Mhz Mhz Mhz Mhz 50 30 15 10 6 ns ns ns ns ns La memoria e' organizzata con milioni di indirizzi, nei quali vengono memorizzati i singoli byte, quando il processore richiede i dati viene specificato l'indirizzo, il tipo di operazione e quindi trasferiti i dati, per far tutto questo occorre un certo tempo a causa della lentezza della ram rispetto al processore, questo tempo viene chiamata latenza, per minimizzare questo effetto, si utilizza una tecnica chiamata “accesso in modalita' burst”, vengono letti in successione 4 segmenti relativi all'indirizzamento, questo impedisce il ripetersi della latenza. Le operazioni in modalita' burst vengono misurate dal numero di clock necessari per ciascun segmento ; una notazione burst 8-2-2-2, indica che per il primo segmento e' richiesto untempo di clock di 8 ma per i restanti solo 2, per un totale di 14 cicli per 4 segmenti, contrariamente ai 32 richiesti se non fosse rpesente questo stratagemma. L'accesso in modalita' burst utilizza la cache di livello 2 (L2), per esempio L2 di 256 bit potrebbere ricevere e bufferizzare 2 set di burst o 8 segmenti. Memoria Cache La memoria cache e' molto veloce e sfruttata per contenere istruzioni e dati richiesti di frequente. Lo scopo di queste memorie e' di memorizzare i dati provenienti da un dispositivo più lento al fine di velocizzare il processo di elaborazione. sono delle aree di memoria piccole situate tra memoria primaria e il processore. Questa contiene copie di istruzioni e dati che riceve dalla RAM. La memoria cache nel processore possiamo distinguerla di livello 1 2 3 opera a 'zero wait'state' per ridurre il collo di bottiglia tra processore e memoria. Memoria cache del disco , utilizzata per velocizzare il trasferimento di dati e programmi dal disco alla RAM. Wait States Un Wait states non e' nient'altro che un ciclo extra di clock per dare l'opportunita' al device di completare l'operazione. A volte un singolo Wait state non e' sufficiente. E' il caso del processore e della memoria a differenza di quella cache, la memoria convenzionale SDR / DDR opera ad una velocita' inferiore a quella del processore, questo costituisce un collo di bottiglia Locality of reference Il principio di locality of reference e' molto semplice, e' un criterio di progettazione basato sull'ipotesi che i dati o le istruzioni successive siano probabilmente situate immediatamente dopo gli ultimi dati o istruzioni richieste dalla cpu. Usando questo principio vengono copiati dati e le relative istruzioni nella memoria cache anticipanto le richieste della cpu. Per quanto possa apparire sorprendente i sistemi cache del pc tendono a raggiungere un percentuale di successo che varia dal 90% al 95% Interrupt L'ordinario flusso di un programma, necessita di essere interrotto, affinchè la cpu possa fare qualcos'altro. Per esempio se non stiamo leggendo un libro e squilla il telefono, andiamo a rispondere. Così se paragonato questo esempio ad un telefono cellulare, mentre e' attivo lo screen saver o stiamo scrivendo un sms, si attiva la chiamata, appunto si è verificato un interrupt. Se non ci fossero gli interrupt, dovrò di volta in volta sollevare la cornetta del telefono ed assicurarmi che nessuno mi ha chiamato ?! Ogni qual volta si verifica un Interrupt il computer passa l'esecuzione ad un “interrupt handler” per poi ritornare all'esecuzione normale. Alcuni esempi : – – Tastiera; Sistema sonoro. Comunicazione Le comunicazioni di un processore con altri circuiti elettronici, a titolo esemplificativo, avvengono mediante un numero di “piedini” o pin, ognuno dei quali potra' assumere solamente 2 significati 0 oppure 1. all'interno del computer sono visibili al programmatore un certo numero di registri altri sono nascosti ed usati per funzioni speciali. Mostrero' in tabella alcuni pin di collegamento e loro significato : modello a 32 bit : SIMBOLO TIPO Nome e Funzione Aa – A0 uscita ADDRESS specifica l'indirizzo nello spazio di i/o D31 – D0 bidirezionali DATA supportano i dati nello spazio in memoria e in quello di i/o L1 – L0 uscita LENGTH specificano il numero di byte da trasferire MR uscita MEMORY READ indica che il processore compie una operazione di lettura MW uscita MEMORY WRITE indica che il processore compie una operazione di scrittura IOR uscita I/O READ indica che il processore nello spazio di I/O esegue una oprazione di lettura IOW uscita I/O READ indica che il processore nello spazio di I/O esegue una oprazione di scrittura RESET ingresso RESET riporta il processore nello stto iniziale Schema di funzionamento umano : analogico ; schema fi funzionamento elettronico : digitale . Funzionamento Il processore preleva dalla memoria una istruzione alla volta, seguendo il principio di locality reference, essa viene trasferita dallo spazio fisico in memoria in un apposito registro IR (instruction register), attraverso una o più oprazioni di lettura. L'indirizzo della locazione da cui prelevare l'istruzione e' indicato da un registro PC (program counter). Dopo ogni operazione di lettura questo viene incrementato in modo che punti alla prossima istruzione sequenzialmente. Successivamente li'istruzione viene interpretata ed eseguita. processore Memoria I/O Aa-A0 ----| Aa-A0 ---| Aa-A0 ---| D31-D0----| D31-D0---| D31-D0---| L1-L0 ----| L1-L0 ---| L1-L0 ---| MR ----| MR ---| MR ---| MW ----| RD ---| RD ---| IOR ----| WR ---| WR ---| IOW ----| | | | | | ----------------------------------| Address BUS Data BUS Length BUS MR BUS MW BUS IOR BUS IOW BUS Immagine PIC : Come avete visto dalla tabella precedente i tre componenti principali comunicano tra loto mediante i BUS . Il Bus connette i vari componenti VNA : essenzialmente analizzero' i tre di pi di bus : (VNA Vonn Neumann Architecture) – – – BUS indirizzi ; BUS dati ; BUS controllo ; BUS DATI Il bus dati come indica il termine serve a scambiarsi i dati attraverso le varie parti del computer. La lunghezza o ampiezza del bus viene misurata in bit nella famiglia x86 il bus dati varia da 8, 16, 32 e 64 bit o linee; con questo ultimo termine linee intendo la capacita' simultanea di trasferire piu' dati alla volta. Se ne deduce che piu' il bus dati e' ampio maggior sara' la velocita' di traferimento dati in una macchina. tabella architettura x86 e bus dati x86 bus 8088 80188 8086 80186 80286 80386sx 80386dx 80486 Pentium 8 bit 8 bit 16 bit 16 bit 16 bit 16 bit 32 bit 32 bit 64 bit BUS INDIRIZZI Il bus dati trasferisce i dati da una locazione di memoria ad un altra, ma queal'e' quest'ultima? il bus indirizzi dice esattamente dove prendere i dati; indica l'esatta posizione nello spazio fisico di memoria. Quindi il bus indirizzi indica quanta memoria iul computer puo' indirizzare. tabella x86 e indirizzamento in memoria. Processore Bus Indirizzi Memoria indirizzabile CS:IP 8088 20 1.048.576 1 MEGABYTE FF:FFFF:FFFF 8086 20 1.048.576 80188 20 1.048.576 80186 20 1.048.576 80286 24 16.777.216 80386sx 24 16.777.216 80386dx 32 4.294.976.296 80486 32 4.294.976.296 Pentium 32 4.294.976.296 16 MEGABYTE FFFF:FFFF:FFFF 4 gigabyte FFFF:FFFF:FFFF:FFFF BUS CONTROLLO Si puo' definire il BUS di controllo come una collezione di segnali, che controllano come il processore comunica con gli altri circuiti logici. In particolare il processore deve sapere come deve comportarsi con i dati e gli indirizzi. Una volta che il processore ha acquisito le informazioni relative ai bus DATI e indirizzi, deve sapere se Leggere o Scrivere questi. Appunto il BUS di controllo che contiene diversi pin al suo interno indica delle informazioni aggiuntive su questi dati. Memoria e memorizzazione Un tipico processore x86 indirizza al massimo un megabyte di memoria, come puoi vedere dalla tabella precedente in relazione al tipo di processore ed alle linee che e' in grado di indirizzare. La memoria e' un insieme sequenziali di locazioni da zero fino ad un massimo relativo alle linee di memoria indirizzabili. Perciò possiamo paragonare la memoria ad un array di byte : – unsigned char (0,1048575) notazione in C. Ricordo che l'ammontare piu' piccolo dato che un computer puo' indirizzare e' un byte, quindi se ho la necessita di leggere 4 bits dovrò leggere un byte, se ho la necessita' di leggere 16 bits una word, leggerò 2 byte. Una Particolarità da tenere in considerazione e' che la famiglia x86 memorizza il byte piu' piccolo prima del byte piu' grande. Per esempio se ho i seguenti numeri in word FF00h 34D8h saranno memorizza cosi : 00FFD834. Clock di Sistema L'elaboratore elettronico e' composto da diversi circuiti elettronici operanti a velocita' diverse, occorre quindi un modo per sincronizzare il tutto. Il clock di sistema mantiene sincronizzate tutte le operazioni del computer. L'orologio di sistema alterna il suo moto tra zero e uno, questa periodo viene definito frequenza di clock. Un periodo completo viene anche chiamato ciclo di clock. Un tipico Pentium 4 3.0 G/hz corre ad una velocità di 3 miliardi di clock al secondo. L'alternanza della fasi viene anche chiamata “falling Edge” da 1 a 0 e “Rising Edge” da 0 a 1. La cpu impiega piu' tempo nelle fasi in cui e' zero o uno che non nell'alternanza. Percio' l' Edge Clock e' un punto perfetto di sincronizzazione. Dato che tutte le operazioni delle cpu sono legati al clock di sistema si evince che non e' possibile eseguire porocessi piu' veloci che non quello indicato dal clock di sistema. Segmenti e memoria Ritorniamo indietro di qualche decennio, l'8088 e 8086 divide la memoria in moduli da 64K chiamati segmenti. Questo perche' il numero piu' alto che questo processore poteva indirizzare era 65535 appunto 0FFFFh o 64K. Tuttavia questo processore puo' indirizzare fino a 1 mega di memoria che per i tempi era una quantita' spropositata. Questo e' possibile grazie ad un piccolo trucco che utilizza due numeri denominati selettore e offset, due registri appunto. La memoria veniva suddivisa in 16 banchi che venivano gestiti dal selettore e un per l'offset: quindi 16 * 65535 appunto 1 mega di memoria. (anche se utilizzando 2 registri la memoria indirizzabile poteva essere 4 gigabyte). A titolo informativo i segmenti nella terminologia informatica vengono chiamati “kludge” ossia soluzione imporovvisata ad un problema. Il microprocessore 80386 utilizza altri tipi di indirizzamento che sono molto piu' semplici e non utilizza i segmenti. ma veniamo a noi. Nell'8088 l'istruzione da prelevare dallo spazio fisico viene identificata da due registri CS:IP (code segment e Instruction Pointer). La memoria viene suddivisa in 16 segmenti vedi tabella : segmento selettore * 16 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 10h 20h 30h 40h 50h 60h 70h 80h 90h 100h 110h 120h 130h 140h 150h 160h 16 32 48 64 80 96 112 128 256 272 288 304 320 336 352 368 L'indirizzo corrente si ottiene con questa formula CS * 16 + IP Esempio CS = 0FFFFh e IP = 0FFFFh 0FFFFh * 0Fh + 0FFFFh = 0FFFF0H = 1.048.560 un mega in decimale E' strano questo metodo per indirizzare la memoria, tuttavia funziona. Vorrei ringraziare : Peter Norton e John Socha, che tramite il loro libro “Linguaggio Assembly per pc IBM”, “Assembly avanzato” ho iniziato ad apprezzare la programmazione in assembly. Un problema nasce dal fatto che gli indirizzi vanno “normalizzati”. se abbiamo un indirizzo fisico 0x1cf0d lo stesso nella codifica tramite segmento:offset potrà essere 0x1234:abcd oppure 1000:cf0d per ovviare questo problema, sono stati creati molti modi per convertire un indirizzo fiiso (16 in totale); questo illustrato e' quello di norma più utilizzato : il segmento l'offset deve essre un multiplo di 16 deve essere un valore intero da 0x00 a 0xf0h Esempio : indirizzo fisico normalizzazione 0x1cf0d 1000:cfd0 1 cf0:d 1cfd0:0 risultato 1000:cf0d 0x1cfd0 Tra i vari modelli di memoria, in riferimento all'8086 e quindi in che modo un applicazione gestisce la memoria individuiamo : – Modello TINY : Il segmento dati e quello del codice erano contenuti in 64K/B, emulava il modo di operare dell'8080. – Modello SMALL : Questo era il tipo di modello prevalente, in quanto permetteva di raddoppiare le dimensioni dei programmi, 64 K/B dati e 64 K/B codice, mantenendo un indirizzamento a 16 bit. – – – Modello compact : Un segmento di codice e più segmenti per i dati Modello Medium : ES=DS, più segmenti per il codice – Modello LARGE : Questo modello consentiva l'uso di segmenti di codice e dati multipli. in questo modello era possibile indirizzare fino a 1 M/B, più segmenti per il codice e per i dati. – Modello Huge dati. - Modello Flat : un singolo array può essere maggiore di 64k; più seg. codice; o più seg. : tipico dell'386 senza limitazioni, modalità virtuale. 80286 Dopo l'avvento del processore 8086, nel 1982 venne introdotto un processore compatibile, appunto 80286, che forniva un aumento delle prestazioni e poteva lavorare in due modalita' differenti : – – La modalità reale (quella di default) La modalità protetta . Modalità protetta : Il modo protetto espande la dimensione della memoria fisica indirizzabile da 1 M/Ba 16 M/B, permettendo l'impiego della memoria virtuale e fornisce la possibilità di separare i processi (TASK) in un ambiente multiutente (MULTITASKING). 80386 La capacità di inrizzamento con questo processore e' notevolemente aumentata, sfruttando segmenti da 32 bit è possibile ora indirizzare 4 G/B di memoria. Con questo nuova architettura i progettisti hanno dovuto affrontare da subito due problematiche : – – – Compatibilità ; Prestazioni ; L'80386, può anche operare in modalità protetta. In questa modalità ogni segmento viene marcato da un bit che specifica che il segmente è in modo protetto; cioè contiene codice x286 o x386. Modalità Virtuale : Ad Eccezione di quando si opera in modo reale, l'x386 viene considerato un processore virtuale. Quando un'istruzione richiede il contenuto di una locazione dimemoria, questa farà riferimento a tale locazione non solo utilizzando un indirizzo hardware di memoria fisica, ma anche mediante un indirizzo virtuale. L'indirizzo virtuale, non e' nient'altro che un nome per una locazione di memoria, il processore traduce questo nome in una locazione fisica. L'indirizzo virtuale in un x386 viene individuato da 2 numeri un selettore e un scostamento (OFFSET ). La cpu traduce un indrizzo virtuale in un singolo numero a 32 bit che prende il nome di indirizzo lineare. Senza entrare nei dettagli la cpu x386 utilizza il selettore come un indice per una serie di tabelle, Tabelle dei descrittori. sostanzialmente un descrittore è un blocco di memoria che descrive le caratteristiche di un determinato elemnto del sistema : – – – – Indirizzo base ; Limite ; Autorizzazione di accesso ; Livelli di privilegio. REGISTRI Il processore possiede tre ipi di registri : – – – i registri generali ; i registri selettore ; i rgistri di stato ; Registri generali Questi sono utilizzati per memorizzare dati. I registri generali sono 8 : (questi hanno una capacita' di 32 bit) ultimamente e' stato introdotta la tecnologica x86-64bit con registri a 64 bit. (RAX...) Ogni registro ha una parte bassa ed una alta, relativamente al registro EAX possiamo avere la parte bassa AX a 16 bit e le due parti indicante Il byte alto e basso denominati AH e AL. Alcuni registri generali vengono utilizzate da alcune istruzioni in modo specifico 32 bit 16 bit 8 bit / 8 bit – – – – – – – – EAX EBX ECX EDX EBP ESI EDI ESP AX BX CX DX BP SI DI SP AH / AL BH / BL CH / CL DH / DL Denominazione/Descrizione Accumulatore Base Utilizzato come conteggio in alcune istruzioni Utilizzato come indice Base Stack Pointer Indice Indice utlizzato in concomitanza con il regitro selettore SS Registro di stack Registri selettori I registri selettori sono destinati a contenere i selettori dei segmenti essi sono 6 : – CS – – – – – SS DS ES FS GS CODE SEGMENT SELETTORE CODICE CORRENTE,abbinato al registro IP identifica l'istruzione corrente STACK SEGMENT SELETTORE PILA CORRENTE, segmento in cui risiede lo stack. DATA SEGMENT SELETTORI DEI SEGMENTI DATI CORRENTI, EXTRA SEGMENT SELETTORI DEI SEGMENTI DATI CORRENTI, Extra Segment SELETTORI DEI SEGMENTI DATI CORRENTI, SELETTORI DEI SEGMENTI DATI CORRENTI. Registri di Stato – IP – F Contiene l'offset del segmento di codice a partire dalla quale sara' prelevata la prossima istruzione CS:IP. Flag register . Questo registro ha più elementi significativi Lo stack Prima ho accennato alla coppia di registri ESP:EBP. Nella programmazione e' molto utile disporre di una pila o stack, dove memorizzare temporaneamente i dati tra una chiamata e l'altra o per ripristinare alcuni reigistri. Lo stack utilizza la tecnica LIFO Last in First out, cioè l'ultimo che entra e' il primo ad uscire. Contrariamente a quanto si possa pensare lo stack cresce diminuendo le sue dimensioni per esempio se il registro SP e' impostato a 0FFFFh memorizzando una word esso viene impostato a 0FFFDh che punta al prossimo elemento da memorizzare, quindi per recuperare il dato, occorrera' riferiscei come sp+4 aggiungere 4 allo stack pointer. Lo stack come vedremo piu' avanti viene manipoalto da due istruzione push e pop che inseriscono ed estraggono i dati dallo stack. BIT 0 Flag Tipo Descrizione CF Carry Questo indica che un riporto negtivo o positivo borrow e' stato generato dal bit piu' significativo PF Parity Quando vale 1 l'ultima istruzione ha generato un numero pari di bit AF Auxiliary Riporto ausiliario utilizzato nell'aritmetica BCD e' il riporto generato dal bit n°3 verso il bit n°4. il suo posizionamento dipende dal registro AL. ZF Zero Quando vale 1 indica che l'ultima istruzione ha generato un risultato di zero SF Sign Quando vale 1 indica che nell'utlima operazione il bit piu' significativo e' 1 TF Trap impone se 1 al processore il modo di esecuzione passo-passo. per modificare tale registro ovvorre farlo tramite l'intemediazione dello stack modificare il bit interessato e poi riprenderlo IF Interrupt Se 1 le interruzioni esterne di tipo INTR sono abilitate (settato da CLI e STI) DF Direction controlla la direzione di una stringa di dati se DF=1 allora SI – e DI -se DF =0 allora SI++ e DI ++ Si possono settare con queste istruzioni CLD eSTD OF Overflow Quando 1 indica che durante l'ultima operazione si e' avuto un overflow 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 IOPL Controlla gli accessi in ingresso uscita e rappresenta il livellol di privilegio più basso IOPL NT Nested TASK indica se l'istruzione opera all'interno dello stesso task MSW Registro parola di stato macchina 0 PE Protected mode enable (x286) indica che x286 si trova nel modo di indirizzamento protetto. 1 MP Monitor Prcessore extension, indica un processore di estensione. 2 EM Emulate processore extensio indical'emulazione di un processore. 4 TS Task Sel Utilizzato in presenza di un processore estensione. Il linguaggio ASSEMBLY L'unico linguaggio che e' in grado di capire la cpu e' quello numerico, appunto numeri memorizzati in indirizzi di memoria che altro non sono che numeri. Ogni istruzione e' associata ad un opcode univoco che identifica la sua funzione, cambiano anche per la stessa istruzione se interagisce con la memoria o con i registri, o effettua vari tipi di indirizzamento. L'introduzione di dati/istruzioni come opcode, cioè semplici numeri, risulterebbe alquanto difficile e non privo di errori per esembio movl %ebx,%eax = 0x89h 0xc3h. Quindi al fine di rendere più accessibile tale linguaggio da parte dei programmatori e' stato ideato il linguaggio Assembly. Il linguaggio Assembly e' composto da simboli facili da ricordare e tuttavia vicini al modo di pensare delle macchine (scusate questa lo presa da matrix!), che aiutano la stesura e la manutenzione del codice. Per inserire questi operandi abbiamo bisogno di un Assemblatore, che è un programma che leggendo il codice sorgente in assembly lo codifica in formati binario direttamente interpretabile dalla macchina. Tuttavia, l'assemblatore a differenza dei linguaggi di alto livello è legato all'hardware della macchina, quindi difficilmente riuscirò a ricompilare il codice su un'architettura differente. In questo libro faccio riferimento all'assemblatore GAS e al linker di sistema LD della debian 3.1, ancora per alcuni scopi illustrativi ho utilizzato il NASM e il gcc (compilatore c/c++). Esistono sul mercato diversi tipi di assemblatori, per macchine differenti con diversi caratteristiche formati binari : ELF , COFF ; Modalità scrittura dati : (littile / big Endian) Modalità ordine registri : AT&T, Intel Big / Little Endian Rappresentation Ci sono 2 diversi modi di memorizzare i dati nei computer, Big Endian è il metodo che sembra più facile, In questo sistema il Biggest, cioè più grande , meglio più significante, è memorizzato per primo, per esempio la word 0xffaa è memorizza come FFAA, in questo caso i dati in memoria sono come li pensiamo. Molti processori RISC utilizzano questo metodo. Tuttavia I computer basati su INTEL utilizzano il metodo little endian. Il byte meno rappresentativo viene memorizzato per primo. Se facciamo riferimento all'esempio precedente 0xffaa la codifica in memoria sarà AAFF. Ulteriore Esempio per quanto riguarda le Double Word : Numero word 0xFFAA dword 0x0102ABCD Little Endian Big Endian FFAA 0102ABCD AAFF CDAB0201 C'è una comoda istruzione a partire dal .386 in su che swappa i byte da un formato all'altro. BSWAP BSWAP : Inverte il byte alto e quello basso nella stessa word (32 bit). sintassi : BSWAP esempio : BSWAP %edx (reg. gen) Il set di istruzioni x86 Istruzioni base 8086 Spostamento : - MOV, PUSH, POP, XCHG, LEA, LDS, LES; Aritmetiche : - ADD, SUB, MUL, IMUL, DIV, IDIV, ADC, SBB, INC, DEC, NEG, BCD,DAA, DAS, AAA, AAS, AAM, AAD; Booleane : - AND, OR, XOR, NOT ; Rotazione / Traslazione : - SAL, SAR, SHL, SHR, ROL, ROR, RCL, RCR ; Confronto : - TST, CMP ; Salto : - JMP, Jxx, JCXZ, CALL, RET, IRET, LOOPxx, INT, INTO ; Stringhe : - LODS, STOS, MOVS, CMPS, SCAS ; Condizione : - STC, CLC, CMC, STD, CLD, STI, CLI, PUSHF, POPF, LAHF, SAHF ; Varie : - CWD, CBW, XLAT, NOP, HLT, ESC, IN, OUT, WAIT ; Le nuove istruzioni del 286 PUSHA POPA ENTER LEAVE BOUND VERR/VERW ARPL CLTS LAR SMSW/LMSW SGDT/SLDT/SIDT LGDT/LLDT/LIDT LSL STR/LTR Push di tutti i registri (AX,CX,DX,BX,SP,BP,SI,DI) sullo stack ; Pop di tutti i registri (DI,SI,BP,SP,BX,DX,CX,AX) dallo stack; contatore, profondità Predispone lo stack per eseguire una procedura ; Svuota lo stack alluscita della procedura ; Registro, indirizzo controlla i limiti dei vettori ; Verifica se un segmento di memoria è leggibile o scrivibile ; Aggiusta il campo RPL del selettore; Pulisce il flag Task Switched nel registro CR0 ; Carica i permessi di accesso ; Salva/carica la Machine Status Word ; Salva la Global/Local/Interrupt Descriptor Table ; Carica la Global/Local/Interrupt Descriptor Table ; Carica il limite del segmento ; Salva/carica il registro dei task. Le nuove istruzioni del 386 FS GS registri di segmento registro di segmento PUSHAD/POPAD PUSHFD/POPFD BSF/BSR Btx bit ; CDQ/CWDE Push e pop di tutti i registri estesi ; Push e pop del registro EFLAGS ; Analizzano gli operandi avanti e indietro cercando bit 0 ; Gruppo di 4 istruzioni per leggere, settare, pulire e complementare singoli MOVSX/MOVZX SETcc SHLD/SHRD Lxx IRETD Convertono word in doubleword e doubleword in quadword con estensione del segno ; Muovono bit in strutture più lunghe con estensione di segno o zero ; Gruppo di 30 istruzioni che valutano una condizione per scrivere un byte 0 o 1 nella destinazione ; Traslazioni di dati a 32 bit a sinistra/destra ; Gruppo di 3 istruzioni per caricare il selettore di segmento in FS, GS o SS ; Ritorno da un interrupt a 32 bit ; Le nuove istruzioni del 486 BSWAP XADD CMPXCHG INVD/INVLPG WBINVD Scambia 2 byte ; Scambia e somma ; Confronta e scambia ; Invalidano la cache dati e l'entry del TLB ; Esegue il Write-Back ed invalida la cache dati ; Le nuove istruzioni del Pentium RDMSR/WRMSR RSM CMPXCHG8B CPUID Legge o scrive un registro specifico del modello ; Ritorna dal modo di gestione del sistema; Confonta e scambia 8 byte; Identifica la CPU ; Le nuove istruzioni del Pentium Pro FXSAVE/FXRSTOR SYSENTER SYSEXIT Salva/ripristina lo stato dei registri Floating-Point ; Chiamata di sistema veloce ; Ritorno da chiamata di sistema veloce ; Le estensioni MMX Le estensioni MMX (MultiMedia eXtension) sono un set di 57 nuove istruzioni mirate a velocizzare alcune operazioni grafiche e di comunicazione; esse rappresentano la più importante modifica al set di istruzioni dai tempi del 386. È impiegato un approccio SIMD (Single Instruction Multiple Data) che permette di eseguire la stessa operazione su 2, 4 o 8 operandi interi contemporaneamente. Per fare ciò, tutti gli operandi devono essere caricati nello stesso registro a 64 bit (si possono quindi usare 8 operandi da 8 bit, detti packed byte, 4 operandi da 16 bit, detti packed word, 2 operandi da 32 bit, detti packed doubleword, o infine un unico operando a 64 bit detto quadword); tali operandi verranno tutti elaborati in parallelo senza cali nelle prestazioni. Ad esempio è possibile caricare in un registro 8 diversi operandi a 8 bit (ad esempio delle variabili di tipo char) e sommare in un solo ciclo di clock lo stesso valore a tutti contemporaneamente (alcune operazioni richiedono più cicli). C'è da notare che per evitare lo spreco di spazio non sono stati introdotti nuovi registri, ma gli 8 registri utilizzati dalle operazioni MMX (indicati come MM0-MM7 ) sono stati rimappati sui preesistenti registri a 80 bit utilizzati per le operazioni in virgola mobile (FP0-FP7). Di conseguenza non è possibile utilizzare contemporaneamente istruzioni in floating-point ed istruzioni MMX. Le 57 istruzioni MMX sono raggruppate nelle seguenti categorie: Spostamento Aritmetiche Confronto Conversione Logiche Traslazione Stato MMX Void MOVD, MOVQ PADD, PADDS, PADDUS, PMADD, PMULH, PMULL, PSUB, PSUBS, PSUBUS PCMPEQ, PCMPGT PACKSSDW, PACKSSWB, PACKUSWB, PUNPCHK, PUNPCKL PAND, PANDN, POR, PXOR PSLL, PSRA, PSRL EMMS Le istruzioni SSE del Pentium III Il Pentium III introduce il set di istruzioni SSE (Streaming SIMD Extension), composto da 70 nuove istruzioni. Ben 50 di queste nuove istruzioni operano in modalità SIMD su numeri in virgola mobile a singola precisione. Il loro compito è accelerare alcune operazioni particolarmente utilizzate nel campo della grafica tridimensionale e dell'elaborazione audio. Si tratta in genere di calcoli in cui la stessa operazione deve essere ripetuta più volte su dati differenti. Per le operazioni in virgola mobile su questi registri sono presenti nuove istruzioni di addizione, sottrazione, moltiplicazione, divisione, radice quadrata e approssimazione rapida del reciproco e della radice quadrata inversa. Altre 12 istruzioni estendono il set di comandi MMX originario e operano su interi accellerando calcoli specifici della riproduzione video. Sono state introdotte nuove istruzioni per gli interi (in particolare sommatoria di differenze assolute e calcolo della media) così da velocizzare le funzioni di compensazione e stima di moto, caratteristiche della codifica MPEG- 2. Infine altre 8 istruzioni consentono, al software che ne faccia uso, di controllare esplicitamente il flusso dei dati dalla memoria centrale al processore attraverso la cache. In questo modo è possibile ad esempio evitare che la memoria cache si liberi di dati che dovranno poi essere riutilizzati, risparmiando preziosi cicli di clock. Accompagnano queste istruzioni otto nuovi registri da 128 bit, ciascuno capace di contenere quattro valori in virgola mobile a singola precisione. Su questi nuovi registri le istruzioni SSE possono operare in modalità SIMD (Single Instructions Multiple Data): la stessa istruzione applicata a più dati contemporaneamente). Intel affianca così le proprie istruzioni SIMD applicate ai valori in virgola mobile a quelle 3DNow! di AMD e a quella AltiVec del PowerPC (le precedenti istruzioni MMX sono infatti SIMD ma operano solo su valori interi). Sia AMD sia Intel trattano i numeri in virgola mobile in una modalità grazie alla quale un risultato di underflow (un errore che si verifica quando il numero ottenuto è troppo piccolo per essere conservato nel registro) viene automaticamente posto a zero, evitando così la generazione di un errore nel programma. Questa modalità è particolarmente utile nelle applicazioni 3D, per le quali non è essenziale la precisione assoluta del risultato. Le SSE possono però anche operare in maniera convenzionale. In più c'è l'introduzione di una nuova modalità operativa, cosa che non accadeva dai tempi del 386, grazie alla quale è possibile eseguire le istruzioni SIMD in virgola mobile contemporaneamente a quelle classiche a doppia precisione o a quelle MMX, cosa invece impossibile nell'architettura 3DNow! di AMD, visto che i registri sono condivisi. Aritmetiche ADDPS, ADDSS, SUBPS, SUBSS, MULPS, MULSS, DIVPS, DIVSS, SQRTPS, SQRTSS, MAXPS, MAXSS, MINPS, MINSS Logiche Confronto Mescolamento Conversione Spostamento ANDPS, ANDNPS, ORPS, XORPS CMPPS, CMPSS, COMISS, UCOMISS (Shuffle) SHUFPS, UNPCHKPS, UNPCKLPS CVTPI2PS, CVTPI2SS, CVTPS2PI, CVTSS2SI MOVAPS, MOVUPS, MOVHPS, MOVLPS, MOVMSKPS, MOVSS Gestione dello Stato LDMXCSR, FXSAVE, STMXSCR, FXSTOR Controllo della Cache MASKMOVQ, MOVNTQ, MOVNTPS, PREFETCH, SFENCE SIMD su interi (MMX esteso) PEXTRW, PINSRW, PMAXUB, PMAXSW, PMINUB, PMINSW, PMOVMSKB, PMULHUW, PSHUFW Le istruzioni SSE2 del Pentium 4 Con il Pentium 4 sono state introdotte ben 144 nuove istruzioni, suddivise anche questa volta tra istruzioni SIMD su numeri in virgola mobile, istruzioni SIMD su interi ed istruzioni per il controllo della cache. Rispetto alle istruzioni SSE, però, le istruzioni SSE2 operano su dati sia interi che in virgola mobile a 128 bit (a doppia precisione); questa caratteristica le rende particolarmente indicate, oltre che per la riproduzione video e la codifica della voce, per applicazioni scientifiche, di calcolo ingegneristico e di cifratura dei dati. SIMD Floating-Point ADDPD, ADDSD, ANDNPD, ANDPD, CMPPD, CMPSD, COMISD, CVTPI2PD, CVTPD2PI, CVTSI2SD, CVTSD2SI, CVTTPD2PI, CVTTSD2SI, CVTPD2PS, CVTPS2PD, CVTSD2SS, CVTSS2SD, CVTPD2DQ, CVTTPD2DQ, CVTDQ2PD, CVTPS2DQ, CVTTPS2DQ, CVTDQ2PS, DIVPD, DIVSD, MAXPD, MAXSD, MINPD, MINSD, MOVAPD, MOVHPD, MOVLPD, MOVMSKPD, MOVSD, MOVUPD, MULPD, MULSD, ORPD, SHUFPD, SQRTPD, SQRTSD, SUBPD, SUBSD, UCOMISD, UNPCKHPD, UNPCKLPD, XORPD. SIMD su interi MOVD, MOVDQA, MOVDQU, MOVQ2DQ, MOVDQ2Q, MOVQ, PACKSSDW, PACKSSWB, PACKUSWB, PADDQ, PADD, PADDS, PADDUS, PAND, PANDN, PAVGB, PAVGW, PCMPEQ, PCMPGT, PEXTRW, PINSRW, PMADD, PMAXSW, PMAXUB, PMINSW, PMINUB, PMOVMSKB, PMULH, PMULL, PMULUDQ, POR, PSADBW, PSHUFLW, PSHUFHW, PSHUFD, PSLLDQ, PSLL, PSRA, PSRLDQ, PSRL, PSUBQ, PSUBS, PSUB, PSUBUS, PUNPCKH, PUNPCKHQDQ, PUNPCKL, PUNPCKLQDQ, PXOR. Controllo della Cache MASKMOVDQU, CLFLUSH, MOVNTPD, MOVNTDQ, MOVNTI, PAUSE, LFENCE, MFENCE. Desidero ringraziare : Fabrizio Fazzino, per la gentile concessione di materiale informatico, preso dal suo sito : http://www.fazzino.it CAPITOLO 2 Sistemi Numerici SISTEMI NUMERICI prefazione Prima di imparare a programmare, cioè istruire l'hardware su come fare ciò che già sa fare; occorre sintonizzarsi sulla sua lunghezza d'onda affinchè ci sia comunicazione da ambo le parti, al fine di raggiungere il goal prefissato. Anche se questo termine puo' sembrare inusuale, rispecchia molto il modo in cui non ci approcciamo alle persone tramite il linguaggio; Scambiamo informazioni attraverso un medesimo canale comunicativo, bidirezionalmente; In ugual modo questa comunicazione avviene tra opertatore e computer, tramite un linguaggio ben definito che e' quello dei numeri! Infatti l'unica lingua con cui e' possibile parlare con il computer e' un linguaggio numerico e più precisamente un linguaggio fatto di soli 0 (zero) e 1 (uno) che viene definito come il sistema binario. Un computer può indirizzare milioni di locazioni di memoria e queste contengono esplicitamente solo numeri, il personal computer non fa distinzione tra codice e dati, il significato di un numero piuttosto che un altro dipende dalla decodifica dell'istruzione cui il programma fa riferimento ad un determinato indirizzo di memoria. Noi siamo abituati a pensare tramite il sistema decimale a base 10, mentre un pc non lo fa esso utilizza il sistema binario a base 2. Tuttavia come vedremo in seguito per poter codificare anche poche informazioni, occorrono diverse sequenze di BIT* (zero o uno), questo ovviamente porta ad un notevole problema da parte di un operatore umano relativamente alla programmazione e manutenzione del software, per questo si e' deciso di utilizzare un sistema numerico che potesse ovviare a questo inconveniente adottando il sistema numerico esadecimale, composto cioè da 16 cifre ( numero da 0 – 9 e lettere da A – F ), in questo modo risulta possibile impacchettare lunghe sequenze di bit in operandi di piu' facile memorizzazione; Altro sistema numerico utilizzato in un pc e' il sistema ottale a base 8 ( numeri da 0 a 7 ). *BIT e' l'acronimo di Binary Digit, Cifra Binaria ed e' la piu' piccola unità di dato. Sistema Decimale Noi utilizziamo nella vita comune di tutti i giorni il sistema decimale, quando incontriamo il numero 100 (cento) noi non pensiamo a 1 0 0, ma generiamo un immagine mentale di quanto il valore 125 puo' rappresentare nella realtà. I valori decimali usano dieci valori numerici da 0 a 9 per esprimere quante volte una data potenza di 10 è inclusa nel numero. La parola decimale deriva da dieci es 125 km/h es 125 metri es 125 FC Numeri decimali Il numero 125 e' composto da unità, decine, e centinaia in ordine di grandezza crescente. 125 125 = = 1*10^2 100 100 20 5 centinaia decine unita' + 2*10^1 + 20 + 5*10^0 +5 E così via con centinaia migliaia ... numeri decimali positivi e negativi Il sitema decimale contempla i numeri positivi e negativi quindi + 125 – 125 anteponendo il segno meno (-/+) difronte al numero in questione numeri decimali reali / interi +125 +125.125 numero decimale intero numero decimale reale 125.125 = 1*10^2 1*10^-1 + 2*10^1 + 2*10^-2 + 5*10^0 + 5*10^-3 125.125 = 100 + 20 + 25 . + 0.1 + 0.02 + 0.005 numeri decimali reali / interi positivi / negativi ex +125.234 -3455.34445 quando i numeri diventono troppo grandi per poterli leggere mettiamo un segno per identificare il mumero in gruppo di tre cifre, migliaia milioni migliardi e così via. 1°000 1°000°000 1°000°000°000 1 migliaio ; 1 milione ; 1 miliardo . Il sistema numerico binario Come dicevo pocanzi nella prefazione i moderni computer utilizzano al suo interno per comunicare e svolgere le normali operazioni il sistema binario composto da una sequenza di 0 e 1 che vengono codificati all'interno del circuito elettronico come : il passaggio o non di corrente elettrica rispettivamente 0 volt e +5 volt. E' vero che con uno zero ed un uno riusciamo a rappresentare poco, per esempio se possiamo identificarmi come Vero / Falso, Bianco / Nero, Positivo / Negativo; On / Off, Start / Stop. Tuttavia con una sequenza di essi possiamo rappresentare una piu' grande varietaà di infomazioni. Quindi il sistema binario (BI = 2) utilizza solo due valori, questa e' una caratteristica vincente che si abbina perfettamente alla funzionalita' dei transistor, i quali possono assumere soltanto due valori. Il computer memorizza un singolo valore binario (0 o 1) in un transistor Non di meno il sistema binario funziona come il sistema decimale ricordate ; il numero 101 è l'equivalente in decimale di 5 : 1*2^2 + 1*2^1 + 1*2^0 = 5 ricorda il numero 125 dell'esempio precedente la sua rappresentazione binaria è questa : 1111101 vediamo ora se corrisponde 1*2^6 = 64 1*2^5 = 32 1*2^4 = 16 1*2^3 = 8 1*2^2 = 4 0*2^1 = 0 1*2^0 = 1 -------------------------------------= 125 A differenza dei numeri decimali che possiamo riferirci ad ogni cifra con un nome esempio : unita, decine centinaia miglia ... e quindo quando leggiamo il numero 125 diciamo “cento venti cinque”, nel sistema binario occorre leggere cifra per cifra quindi 1111101 va letto come uno uno uno uno uno zero uno. Come accennavo in precedenza una sequenza di cifre binarie e' di difficile lettura ed e' facile sbagliare nel riportare una cifra. Come quella nei numeri decimali che anteponiamo un simbolo ogni 3 cifre in questo sistema anteponiamo un punto ad ogni gruppetto di 4 bit che prende il nome di NIBBLE* se la sequenza termina prima della lunghezza del nibble impostiamo le restanti cifre a zero. (*NIBBLE e' una collezione di 4 BIT) 11111101 diventa 0111:1101 = 125 come si vede l'ottetto binario formato da due NIBBLE e' di piu' facile lettura. Questo ottetto come vedremo in seguito si chiama BYTE, ed e' la più piccola quantità di dati indirizzabile da un computer. Se da una locazione di memoria ho necessita di leggere 3 bit dovro' comunque leggere un byte! Floating Point Anche se questo paragrafo andrebbe trattato piu' avanti e' utile introdurre questo metodo di rappresentazione binaria. Ricordate che vi ho accennato ai numeri reali, beh non di meno possiamo fare la stessa cosa in binario : esempio prendiamo il numero 0.123d = 0*10^0 + 1*10^-1 + 2*10^-2 + 3*10^-3 = 0.123 0 0.1 0.02 0.003 = 0.123 cosi possiamo fare in binario numero 0.101b 0*2^0 + 1*2^-1 + 0*2^-2 + 1*2^-3 0 0.1 0.00 0.001 = = 0.101b = 0.101b di contro per convertire il numero binario in floating point a decimale dovro' fare cosi' : 0 * 1 0 parte intera -----------------------------------------1 * 0.5 0.5 parte decimale 0 * 0.25 0 1 * 0. 125 0.125 ------------------------------------------0.675 risultato Tabella conversione da binario in virgola modible a decimale ^ posizione moltiplicatore -1 -2 -3 -4 -5 -6 -7 -8 0.1 0.5 0.01 0.25 0.001 0.125 0.00010.06250 0.00001 0.03125 0.000001 0.015625 0.0000001 0.0078125 0.00000001 0.00390625 per ottenere le restanti altre posizioni non dovete far altro che dividere ulteriormente il numero per 2. Rappresentazione Reali bit tipo size Assembly 32 64 80 float .float double .double double extended .tfloat istruzioni riferimento (fpu) fldl flds fldt IEEE 754 Ufficialmente IEEE Standard for Binary Floating-Point Arithmetic (ANSI/IEEE) o anche IEC 60559:1989 è lo standard più diffuso nel campo del calcolo automatico. Questo tipo di sistema numerico (oggi standard) definisce un insieme di regole, quindi un metodo per rappresentare I numeri in virgola mobile (floating point), compreso I numeri denormalizzati, gli infiniti (NaN acronimo di Not a Number) e definisce ulteriormente le opeazioni applicabili su questi numeri. Essendo numeri in virgola mobile, questo standard definisce ulteriormente 4 metodi di arrotondamento e aggiunge nella sua descrizione 5 tipi di eccezioni. Fomati Floating Point (FP) Esistono quattro formati rappresentabili in questo standard : - numeri a precisione singola (32 bit) ; Minimo richiesto dallo standard numeri a precisione doppia (64 bit) ; precisione singola estesa (>= 43 bit); (poco usato) numeri a precisione doppia (80 bit) ; Detto questo possiamo definire un numero in virgola mobile come rappresentato rispettivamente da 32 ,64 oppure 80 bit. Benchè questi numeri siano differenti per quanto riguarda la grandezza, hanno in comune tre parti : - un bit di segno - un campo esponenete - un campo mantissa s ; e; m; Numeri a precisione singola Nel linguaggio C e anche in assembly questi numeri vengono definite con il termine "float", e sta ad indicare un numero con una dimensione di 32 bit. Facendo riferimento alle regole di comunanza sopra citate possiamo vedere come il numero float è formato : 1 bit - S 8 bit - E 23 bit - M Segno Esponente Mantissa (31) (23-30) ( 0-22) Il bit S specifica il segno, 0 per I numeri positivi e 1 per I numeri negativi. Il primo campo contiene l'esponente in forma intera, essendo costituito da 8 bit consente di rappresentare 256 valori. Essendo il numero formato da questi tre componenti possiamo alternativamente scriverlo come : (-1)^ S x2^E x M A seconda dei valori dell'esponente o del segno possiamo individuare cinque classi. Per quanto riguardai numeri float, diciamo che il primo bit può assumere I valori 0 oppure 1 a seconda si tratti di un numero negativo positivo, poi tocca all'esponente e quindi alla mantissa. Praticamente nella conversione del numero in virgola mobile dobbiamo trovare quel numero, compreso tra 1 e 2 che elevato ad un determinato esponente ritorni il numero di partenza. Caotico vero, ma un esempio semplifica tutto : prendiamo in considerazione il numero 0.085 come possiamo notare è positivo quindi : 1) il bit del segno è 0. 2) passiamo ora all'esponente, costituito da 8 bit, possiamo rappresentare 256 valori, quindi dato 0.085 dobbiamo trovare l'esponente che moltiplicato per un numero (il nostro goal) ritorno appunto 0.085. ecco il risultato : 0.085 * 000000.5 = 0.17 0.085 * 00000.25 = 0.34 0.085 * 0000.125 = 0.68 0.085 * 000.0625 = 1.36 N : 0.085000 Exp : 4 bias : 123 root@Kanotix:~/source# Come potete vedere, dopo 4 passaggi abbiamo ottenuto l'esponente desiderato quindi è vera la seguente formula : 1.36 * 2^-4 = 0.085 Appunto abbiamo trovato l'esponende desiderato, ora dobbiamo solo sottrarre 127. Questo semplice programmino in C illustra il funzionamente : #include <stdio.h> int main ( void ) { double i=0.50 ; double n=0.085 ; int exp=0 ; double res ; do { res = n/i ; printf ("\n %g * %08g = %g ",n,i,res ); ++exp ; i/=2.0; } while (res < 1.0 ) ; printf ("\n\nN : %f ",n) ; printf ("\nExp : %d ",exp) ; printf ("\nbias : %d \n",(-exp)+127 ) ; return 0 ; } Dato che questa notazione deve rappresentare sia numeri piccoli che grandi, l'esponende va da -126 a +127, questo per quanto riguarda I numeri normali. Tuttavia si vengono a creae dei problemi, per risolvere questo inconveniente sono stati introdotti I bias (distorsione) l'esponente viene calcolato ora in base al suo valore con aggiunta 127. Come da esempio. 3) ora non ci resta che calcolare la mantissa : Il numero trovato ora è 1.36 ma 1 è sottointeso quindi in numero che ci interessa è 0.36, da qui possiamo calcolare l'effettivo valore in FP, (ricordate che vengono calcolati I numeriin maniera approssimata). Il mio suggerimento per la codifica è che per 23 volte, (lo spazio della mantissa per un float), calcoliate il doppio del valore e ogni volta che il valore è >= di 1.0 togliete 1 e impostate a 1 il bit rispettivo , ricominciando la moltiplicazione per 2 con un numeo inferiore a 1.0 vediamo I vari calcoli già fatti : 01) 02) 03) 04) 05) 06) 07) 08) 09) 10) 11) 12) 13) 14) 15) 16) 17) 18) 19) 20) 21) 22) 23) <0.72> <1.44> <0.88> <1.76> <1.52> <1.04> <0.08> <0.16> <0.32> <0.64> <1.28> <0.56> <1.12> <0.24> <0.48> <0.96> <1.92> <1.84> <1.68> <1.36> <0.72> <1.44> <0.88> 0.36 * 2 0.72 * 2 0.44 * 2 0 1 0 In questo modo otteniamo tutta la mantissa e in fianco abbiamo già I valori e la posizione dei bit all'interno della mantissa già tutti ordinati. Vi lascio il programmino giocattolo per la trasformazione del numero in mantissa : Quindi a conti fatti abbiamo ottenuto questo numero : segno esponente mantissa 0 01011100001010001111011 01111011 convertiamolo in esadecimale : 0.085 in IEEE 754 = 0x3DAE:147B #include <stdio.h> int main ( void ) { int i ; double n=0.36 ; for (i=0;i<23;i++) { printf ( "\n %02d) ",i+1); if ( n < 1.0 ) { n = (n*2) ; } else { n = (n - 1.0) * 2; }; printf (" <%0.2f>",n) ; } printf ("\n"); return 0 ; } Ora Facciamo il procedimento inverso, dato un numero in IEEE 754 trasf. in FP : 1) numero di partenza : 3DAE:147B 2) conversione in S,E,M : 0 01111011 01011100001010001111011 3) segno 0 = positivo 4) (01111011 ) 123 - 127 = -4 esponente 5) ora bisogna convertire la mantissa in base dieci : 01011100001010001111011 (0*2^-1) + (1*2^-2) + (0*2^-3) + (1*2^-4) ... (1*2^-23) ... non ho eseguito I calcoli ma prevede di generare un numero approssimativamente vicino a 0.36 6) Assembliamo il tutto : 1 * 0.36 * 2^-4 = 0.085 (~) Conversione da un numero decimale con parte reale in binario Questo è un altro esempio per mostrarvi la problematica della codifica. Prendo il numero che mi interessa, lo moltiplico x 2 e mantengo solo la prima cifra dopo la virgola, ripetendo il processo; se volete essere piu' precisi, mantenetele tutte, ma prima finite di vedere questo esempio (loop infinito!) esempio : num.: 0.5873 x 2 = 1.1 parte intera 1 0.1 0.2 0.4 0,8 0.6 0.2 0.4 0.8 x x x x x x x x 2 2 2 2 2 2 2 2 = = = = = = = = 0.2 0,4 0,8 1,6 1.2 0.4 0,8 1,6 parte parte parte parte parte parte parte parte 0 0 0 1 1 0 0 1 0,100101101b intera intera intera intera intera intera intera intera (0,587890625 approssimato) E' facile intuire dalla divisione come si entri in un loop infinito. Una della conseguenze e' che questo numero non puo' essere rappresentato ESATTAMENTE in binario utilizzando un finito numero di BITS (circa 1/3 dei numeri non possono esssere rappresentati correttamente con un numero finito di digit). IEEE (Institure of Elettrical and Electronic Engineers) e' un istituto che ha disegnato uno specifico formato binario per memorizzare i numeri in virgola mobile (floating point). IEEE definisce alcuni formati a seconda della precisione che vogliamo ottenere chiamati FLOAT e DOUBLE infine EXTENDED PRECISION . Da notare che non viene utilizzata la notazione binario con complemento a 2, ma il segno viene indicato direttamente. = utilizza 32 bit per codficare il numero da 0 a 22 da 23 a 30 n. 31 = = = mantissa 8 bits esponente di base 1 bit per il segno DOUBLE PRECISION = utilizza 64 bit per codficare il numero = = = mantissa 9 bits esponente di base 1 bit per il segno FLOAT PRECISION da 0 a 51 da 52 a 62 n. 63 Per quanto riguarda la conversione dei double,occorre fare riferimentoad un bias maggiore (1023). Conversione da binario a decimale e viceversa Per convertire un numero da binario a decimale l'operazione non e' poi tanto complessa, prendo in ordine di grandezza le cifre binarie e le moltiplico per la sua base (2) con potenza crescente da 0 in poi sommando i singoli rifultati ottengo il numero decimale. La situazione contraria e' un poco piu' complessa in quanto necessitero' di qualche operazione in piu'. Per esempio prendiamo in numero decimale 1359 e vogliamo codificarlo in binario, occorrera' dividerlo per 2 avendo cura di preservare il resto fino a quando non sara' piu divisibile per 2. esempio 1359 679 339 169 84 42 21 10 5 2 / / / / / / / / / / 2 2 2 2 2 2 2 2 2 2 = = = = = = = = = = 679,5 339,5 169,5 84,5 42,0 21,0 10,5 5 2,5 1 0,5*2 0,5*2 0,5*2 0,5*2 0,0*2 0,0*2 0,5*2 0,0*2 0,5*2 0,0*2 = = = = = = = = = = 1 1 1 1 0 0 1 0 1 0 ora prendiamo gli ultimi bit generati dalla divisione e disponiamoli in fila fino ad arrivare alla prima divisione. 10101001111 meglio 0101:0100:1111= 1359 meglio 1°359 10101001111 meglio 0101 0100 1111= 1359 meglio 1,359 (lasciando degli spazi nel numero binario e mettendo una virgola in quello decimae US) Tabella binario / decimale 0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 E gli altri numeri ? Come abbiamo visto e' possibile rappresentare qualsiasi tipo di numero intero decimale con quello binario, tuttavia non e' possibile anteporre il segno + o – per indicare se e' positivo o negativo; allora come indicare -128 ? Ricordate che con uno zero o un uno potevame rappresentare un informazione allora perche' non rappresentare + e – meglio positivo e negativo con un 0 o con un 1; per rappresentare un numero negativo, devo porre una restrizione sui numero che utilizzo proprio per poter mettere il segno che mi interessa. Ricordate che il computer puo' indirizzare come minimo 8 bit, poi dipendente dall'architettura della macchina 16, 32 64 bit alla volta e così via. Riprendiamo la tabella precedente tabella binario decimale 0000 0001 0010 0011 0 1 2 3 Ora riscostruiamola aggiungendo un ulteriore nibble e codificando in numeri in ottetti (byte) tabella binario decimale 0000:0000 0000:0001 0000:0010 0000:0011 ... 1111:1111 0 1 2 3 255 Come abbiamo visto in un ottetto e' possibile definire 256 numeri diversi. (da 0 a 255, numeri positivi, codifica in complemento a uno) che vengono chiamati 'unsigned', dovendo suddividere il tutto in numeri negativi e positivi l'unico modo e' di suddividerli meta' in un senso e meta' nell'altro; da da da da 0000:0000 1000:0001 0 -128 a a 127 -1 0 -1 a a 0111:1111 1111:1111 numeri positivi numeri negativi 127 -128 notiamo che l'ottavo bit assume il significato di bit del segno (+/-) potendo cosi codificare i numeri negativi e positivi; piu' o meno puo' essere visto cosi la numerazione. (codifica in complemento a due) da da +(0)000:0000 0 -(1)000:0001 -1 a a +(0)111:1111 127 -(1)111:1111 -128 tabella bit segno 0000:0000 1000:0001 0 -1 0111:1111 1111:1111 0000:0000:0000:0000 1000:0000:0000:0001 0 -1 0111:1111:1111:1111 1111:1111:1111:1111 +127 -128 +32,767 -32768 0000:0000:0000:0000:0000:0000:0000:0000 da 0 0111:1111:1111:1111:1111:1111:1111:1111 a +2,147,483,647 8 bit 16 bit 32 bit 1000:0000:0000:0000:0000:0000:0000:0001 da -1 1111:1111:1111:1111:1111:1111:1111:1111 a -2,147,483,648 Queste sono le effettive quantita' gestibili all'interno di un registro, intesa come unita' di memorizzazione interna del microprocessore; Conversione da positivo a negativo per convertire un numero da positivo a negativo occorre trasformarlo nel complemento a due della sua forma. seguite questi passaggi : 1) invertite tutti i bit del numero da zero a uno e viceversa 2) aggiungete 1 es. +127 0111:1111 1000:0000 +1 1000:0001 = -1 fase 1) fase 2) Un curioso esempio Osservate questo esempio : prendiamo -32768 e vogliamo trasformalo in positivo, quindi ... +1 1000:0000:0000:0000 = -32768 0111:1111:1111:1111 = NOT operation 1000:0000:0000:0000 = -32768 Come ? ma attenzione il valore +32768 non puo' essere rappresentato con un numero 1 16 bit con segno il massimo e' 32767, percio' il microprocessore in questo caso setta a 1 il flag di : signed aritmetic overflow = 1 la domanda che mi viene spontanea a questo punto e' : ma perche' non utilizzare semplicemente l'ottavo bit con un bit di segno e gli altri come numeri vediamo l'esempio + 5 0000:0101 + -5 1111:1011 = -----------------------0000:0000 1 flag di riporto se ignoriamo il flag ri riporto otteniamo il risultato corretto, quindi questo e' stato fatto per poter utilizzare lo stesso hardware per entrambi i tipi di numeri positivi e negativi. Sistema di numerazione esadecimale Come abbiamo visto negli esempi precedenti, maggiore e' un numero decimale, maggiore e' il numero di bit che dovro' utilizzare per codificarlo nel sistema binario. Se voglio rappresentare un numero in decimale 256 mi occorreranno tre cifre mentre per quanto riguarda un numero binario ne avrò bisogno di 8. Con la codifica crescente da 8 a 16, 32 ... manipolare tale sequenze diventa difficile, per risolvere questo inconveniente e' stato introdotto il sistema esadecimale a base 16, riuscendo così ad ottenere numeri e codifiche compatte e di più facile gestione. Il sistema esadecimale è compost dalle cifre numeriche da 0 a 9 e dalle lettere dell'alfabeto da 'A' a 'F' per i restanti 6 numeri . Il sistema numerico esadecimale deriva da “6 e 10”. tabella : dcimale esadecimale 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 7 8 9 A B C D E F Le regole di conversione da un sistema all'altro sono pressoche identiche a patto che cambiamo la base. per esempio se abbiamo un numero in esadecimale FFAAh otterremo l'equivalente decimale così : F * 16 ^ 3 15 * 16 ^ 3 61040 + + F * 16 ^ 2 15 * 16 ^ 2 + + + 3840 A * 16 ^ 1 10 * 16 ^ 1 + 160 + + A * 16 ^ 0 = 10 * 16 ^ 0 = + 10 = 65450 contrariamente per convertire un numero da hex a dec (abbrevviazioni comuni) occorre dividere il numero per 16. esempio 65450 / 4090 / 255 / 15 -> A numero esadecimale 16 16 16 = = = = 0FFAAh 4090 255 15 resto 0,6250 * 16 = 10 resto 0,6250 * 16 = 10 resto 0,9375 * 16 = 15 -> -> -> A A F tabella : binario decimale esadecimale 0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 7 8 9 A B C D E F Questa tabella puo' aiutarvi nella conversione dal sistema esadecimale a quello binario e viceversa il numero precedente 0FFAAh e' il rispettivo binario 1111:1111:1010:1010b ugualmente possiamo prendere un numero binario 1011:1111:0101:1110:1100:0011:1011:1100 b e convertilo nel corrispondente numero esadecimale 0BF5E:C3BCh. Codifica positivo / negativo in esadecimale. Il funzionamento non e' diverso da quello dei numeri binari anzi e' uguale, utilizzando sempre il bit piu' a sinistra, cioe' quello del segno. tabella 8 bit 0000:0000b 1000:0001 00h 80h 0 -1 0000h 8000h 0 -1 0111:1111 1111:1111 7Fh FFh +127 -128 7FFFh FFFFh +32,767 -32768 16 bit 0000:0000:0000:0000 1000:0000:0000:0001 0100:0000:0000:0000 1111:1111:1111:1111 32 bit 0000:0000:0000:0000:0000:0000:0000:0000 0111:1111:1111:1111:1111:1111:1111:1111 1000:0000:0000:0000:0000:0000:0000:0001 1111:1111:1111:1111:1111:1111:1111:1111 tabella : decimale esadecimale 10 20 40 100 128 256 512 1024 4096 16384 32768 65535 0Ah 10h 20h 64h 80h FFh 0200h 0400h 1000h 4000h 8000h FFFFh da 0 0000:0000h a +2,147,483,6477FFF:FFFFh da -1 8000:0000h a -2,147,483,648 FFFF:FFFFh Organizzazione numerica. Ora che abbiamo visto i sitemi piu' importanti in un pc (manca quello a 12 bit ottale) utilizzato in alcuni harware. definiamo le classi di raggruppamento dati. Come nel sistema decima esistono le unià, le decine le centinaia le migliaia ... analogamente per quanto riguarfa il sistema binario e esadecimale esistono tali concetti. BIT Come dicevo il BIT e' la piu' piccola unita rappresentabile in un pc puo' assumere soltanto 2 condizioni (lunghezza 1) NIBBLE Il NIBBLE e' l'equivalende di 4 bit, poco usato (tuttavia lo si puo' trovare nalla codfica BCD) impacchettare il sistema decimale in 4 bit come vedremo piu' avanti. (lunghezza 4 bit) BYTE (B) E' insieme di 8 bit, di fatto un byte quindi contiene 2 nibble, questi vengono suddivisi in High, e Low (alto e basso) a seconda della sua posizione (High Order h.o and Low Order l.o), (lunghezza 8 bit da 0 a 7). Puo' memorizzare numeri da 0 a 256 oppure da -128 a +127. Qui di seguito riporto 8 locazioni di memoria e un suo ipotetico contenuto : Indirizzo Memoria Word 0 FA 1 4D 2 23 3 E4 4 FF 5 00 6 10 7 1F (2 byte) (W) Formata da due byte, in ordine High e low. (lunghezza 16 bit da 0 a 15). Puo' memorizzare numeri da 0 a 65535 o da -32768 a +32767 Double Word (4 byte) (D) Formata da 2 word o 4 byte. (lunghezza 32 bit da 0 a 31). Possiamo trovare il suo utilizzo nelle operazioni in virgola mobile. FLOAT (32 bit) puo Quad Word (8 byte) (Q) Formata da 2 Double Word. Utilizzato nelle operazioni in virgola mobile. DOUBLE (64 bit) Ten byte (10 byte) (T) Utilizzato nel formato a Precisione Estesa di IEEEE. Long double (80 bits) Paragrafo (16 byte) Come indicato, e' formato da 4 word. Pagine (1000 byte) (4096 caratteri decimale) Suddivisione di unita' piu' grosse chiamate segmenti. Tabella solo per curiosita', alcuni nomi utilizzati per altre èarole binarie. bit 1 2 4 5 8 10 16 32 48 64 h.o l.o 7 0 15 31 0 0 63 0 nome hex bit Crumb o tayste nibble F Nickel byte deckel word,playte chawmp double word dynner quad word FF nibble h.o : nibble l.o FFFF FFFF:FFFFh byte word h.o : byte h.o : word l.o l.o FFFF:FFFF:FFFF:FFFFh tabella lunghezza bits BITS 3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 BYTE * * * * * * * * WORD * * * * * * * * * * * * * * * * DWord * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * tabella interi BIT DA A TIPO UNSIGNED BYTE 8 0 255 BYTE SIGNED BYTE 8 -128 127 BYTE UNSIGNED WORD 16 0 65.535 WORD SIGNED WORD 16 -32.768 32.767 WORD UNSIGNED DWORD 32 0 4.294.967.295 DOUBLE WORD SIGNED DWORD 32 -2.147.483.648 2.147.463.647 DOUBLE WORD BIT DA A tabella reali / interi lunghi TIPO FLOAT 32 8.43 * 10^-37 3.37 * 10^38 DOUBLE WORD DOUBLE 64 3.4 * 10^-4392 1.2 * 10^4392 QUAD WORD BCD 80 9.9 * 10^-18 9.9 * 10^18 TEN WORD INTERI CORTI 32 -2 * 10^9 2 * 10^9 DOUBLE WORD INTERI LUNGHI 64 -9 * 10^-18 9 * 10^18 QUAD WORD Estensione di una classe ad un altra Se prendiamo un byte contenente il numero FFh (unsigned) positivo, questo equivale al numero decimale 255. Questo e' esattamente lungo 8 bit, come e' possibile trasformare queto numero in 16 bit? Semplicemente aggiungendo due zeri all'inizio del numero 00FFh quindi senza alcun problema diventa una word.Particolare attenzione sorge quando si trattta di numeri negativi, in cui l'ultimo bit (quello del segno) e' impostato a uno. Prendiamo in considerazione il numero 80h (128t) che e' l'equivalente a -1 come numero negativo, se vogliamo estenderemo a una word occorrerra copiare l'ultimo bit per le restanti posizio, quindi in definitiva il numero risulterà FF80h conservando -1 esempio 1 Tipo dec binario byte binario word positivo negativo 128 -1 1111 1111 FF FF 0000:1111 1111:1111 00FFh 00FFh analogamente se voglio contrarre una word in un byte dovro' mantenere il bit del segno esempio 2 Tipo dec binario word binario byte positivo negativo 128 -1 0000:1111 1111:1111 00FFh 1111b Fh 00FFh 1111b FFh alcuni esempi li vedremo in seguito con le apposite istruzioni di conversione. esempio 3 tipo dec binario word binario byte positivo 256 1111:1111b conversione non possibile 0100h 0000:1111 0Fh negativo 40000 1001:1100:0100:0000b conversione non possibile 9C40h 0000:0000:0100:0000 40h in questo caso non e' possibile in quanto il bit del segno va perso. BCD Binary Coded Decimal E' un sistema che codifca in numeri decimali in una forma binaria. La codifica di una cifra decimale richiede 4 bit. Poichè l'unità più piccola indirizzabile da un elaboratore e il byte, risulterebbe un spreco non utilizzare gli altri 4 bit rimanenti; Quindi si e' pensato di immagazzinare in un singolo byte due cifre decimale. questa rappresentazione viene chiamata BCD packed. Tabella : BCD 0000b 0001b 0010b 0011b 0100b 0101b 0100b 0101b 1010b 1011b Decimale 0 1 2 3 4 5 6 7 8 9 Quindi per rappresentare il numero decimale 99 faro riferimento ai 2 nibble 1011:1011 bcd e non all'effettivo 99 binario 0110:0011b Codice ASCII Abbiamo parlato nei precedenti 2 capitoli, dell'architettura del calcolatore e dei sistemi numeri, ora vediamo come il computer riesce a rappresentare tutti i caratteri, ossia rappresentare in parte il mondo che lo circonda. ASCII è l'acronimo di : American Standard Code Instruction Interchange. Definisce un modo per codificare i caratteri alfabetici e non in numeri binari rappresentabili dal computer. L'ASCII e' diviso essenzialmente in 4 gruppi di 32 caratteri ciascuno : 1° gruppo da 0 a 31 : caratteri speciali (non stampabili) (00) Essi non possono essere stampani iin quanto per lo più eseguono varie operazione di stampa e visualizzazione. Ne e' l'esempio il carattere 13 o Carriage Return (CR) '\r' o il Carattere 10 New Line '\n' oppure il carattere 7 BELL che emette un suono acustico '\7'. Altri ancora BackSpace che muove il ursore indietro di una posizione e LINE FEED che muove il cursore avanti in un dispositivo di ouput , generalemnte la stampante. 2° gruppo da 32 a 63 : comprende caratteri per la punteggiatura e simboli speciali (01) Il secondo gruppo definisce i segni di punteggiatura e alcuni caratteri speciali, tra i caratteri più importanti in questo gruppo e' lo spazio 0x20h (32) e in numeri da 0 a 9 codificati a partire da 0x30h a 0x39h. Da notare che pur riferendosi a tali numeri la corrispondenza con quelli contenuti nell'ottetto non e' 1:1 in effetti se nel registro AH troviamo contenuto 1 non verrà visualizzato nessun carattere bensì dovrò aggiungere 0x30h per poter visualizzare il carattere '1'. Viceversa se dispongo del carattere ascii '1' dovrò sottrarre 0x30h per poter gestire il numero 1. 3° gruppo da 64 a 96 : riservato per i caratteri MAIUSCOLI (10) Il terzo gruppo in parte e' riservato per i caratteri maiuscoli da 'A' a 'Z' da 0x41h a 0x5Ah (65..90) essendo presente comunque 26 caratteri nell'alfabeto gli altri 6 caratteri sono rappresentati da simboli speciali. 4° gruppo da 97 a 128 : riservato per i caratteri minuscoli (11) il quarto ed ultimo gruppo e' riservato per i primi 26 carattere da quelli minuscoli da 0x61h a 0x7Ah, quindi per trasformare un carattere da maiuscolo a minuscolo non dovrò far altro che sottrarre dal primo 0x20h o 32. Tuttavia se andiamo a vedere la codifica binaria dei caratteri, notiamo che essi differiscono solo di un bit piu' precisamente i 5° : Carattere Carattere 'A' 'a' 65 97 7654:3210 0x41h 0100:0001b 0x61h 0110:0001b Mediante le operazione logiche, agendo sul bit 5 possiamo trasformare il numero da maiuscolo a minuscolo e viceversa Carattere 'A' 0100:0001 OR 0010:0000b SUBB 0x20h , %AL → ORB 0b00100000 , %AL → 0110:0001 'a' Carettere 'a' 0110:0001 AND 1101:1111b ADDB 0x20h, %AL → ANDB 0b11011111 , %AL → 0100:0001 'A' La trattazione delle operazione logiche viene fatta nel capitolo 4, tuttavia questo dimostra come poter risparmiare codice a livello di programmazione. C'è un detto che è quasi sempre vero, “meno codice, meno cicli = più velocità” ma non sempre e' così. L'appartenenza ad un gruppo piuttosto che ad un altro e' identificato dal bit 5 e 6 : bit bit 65 descrizione 00 01 10 11 Caratteri di controllo Numeri e punteggiatura Caratteri maiuscoli e speciali Caratteri minuscoli e speciali 7 0 nella codifca ASCII e' sempre zero Tuttavia IBM (International Businness Machine). Estende con altri caratteri dopo questo. vedi tabella ASCII di riferimento. TABELLA CODIFICA ASCII Standard Dec Hex 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F NUL SOH STX ETX EOT ENQ ACK BEL BS HT LF VT FF CR SO SI Dec Hex 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F DLE DC1 DC2 DC3 DC4 NAK SYN ETB CAN EM SUB ESC FS GS RS US Dec Hex 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 20 21 22 23 24 25 26 27 28 29 2A 2B 2C 2D 2E 2F ! " # $ % & ' ( ) * + , . / Dec Hex Dec Hex Dec Hex 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 30 31 32 33 34 35 36 37 38 39 3A 3B 3C 3D 3E 3F 0 1 2 3 4 5 6 7 8 9 : ; < = > ? 40 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F @ A B C D E F G H I J K L M N O 50 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F P Q R S T U V W X Y Z [ \ ] ^ _ Dec Hex 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 60 61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F ` a b c d e f g h i j k l m n o Dec Hex 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 70 71 72 73 74 75 76 77 78 79 7A 7B 7C 7D 7E 7F p q r s t u v w x y z { | } ~ DEL Un secondo trucco nella conversione dei numeri dcimali da 0 a 9 ai rispettivi codice ASCII da '0' a '9' e' settare il nibble alto a 3. numero decimale 7 → binario 0000:0111b high low → codifica asci '7' 0011:0111b tabella Char Decimale Hex High Low '0' 48 0x30h 0011 0000 '1' 49 0x31h 0011 0001 '2' 50 0x32h 0011 0010 '3' 51 0x33h 0011 0011 '4' 52 0x34h 0011 0100 '5' 53 0x35h 0011 0101 '6' 54 0x36h 0011 0110 '7' 55 0x37h 0011 0111 '8' 56 0x38h 0011 1000 '9' 57 0x39h 0011 1001 Carattere Decimale Hex Binario '7' 55 0x37h 0011:0111b Converto '7' a 7 Numero decimale 7 Hex Binario 7 0x07h 000:0111h al=0x37h operazione ANDB 0b00001111 , %AL → %AL = 0x07h al=0x09h operazione ORB 0b00110000, %AL → %al = 0x39h 0000:0111b Converto 9 a '9' 0011:1001b CAPITOLO 3 Pronti Via! Pronti Via! Iniziamo a programmare x86 attraverso L'assembly. I primi capitoli anche se tediosi sono serviti a dare una infarinatura di base per poi passare direttamente alla programmazione; tuttavia ritengo che i concetti devono essere messi in pratica, a che serve pagine e pagine di teoria se poi non riesco a metterla in pratica. L'unico modo che posso consigliarvi e' quello di programmare e soprattutto, “sbagliare” ho imparato molto dai miei errori e a volte mi ha permesso di capire meglio alcuni concetti che davo per superficiali e in particolar modo consiglio di essere curiosi, non accontentarvi di quello che avete studiato e messo in pratica ma approfindite l'argomento fino a sviscerarlo nei piu' piccoli dettagli solo cosi' farete la differenza! Basta ora passiamo a ciò che più amo la programmazione !!! ricetta : 1) prendere un editor di testo, qualsiasi quello che a voi piace di piu' ; 2) introducete questo codice : 3) mescolate ed accendete il fuoco ! File : primo.s # # Primo Programma .section .data output_string: .ascii “Pronti Via!!!\n\0” .section .text .globl _start _start: pushl $output_string call printf addl $4,%esp pushl $0 call exit 3) 4) 5) 6) Salvate il programma con primo.s e compilatelo con il seguente comando as primo.s -o primo.o ld -dynamic-linker /lib/ld-linux.so.2 -o primo primo.o -lc ./primo Dovrete aver ottenuto come output la stringa di testo “Pronti via!”. Passiamo alla spiegazione ora : Il carattere cancelletto “ # “ identifica una linea di commento per altro molto utile e spesso sottovalutato per la manutenzione del codice. Successivamente il listato presenta due sezioni “.section” denominate “.data” e “.text” queste sezioni contengono rispettivamente i dati ed il codice. Nella sezione dati troviamo un etichetta “output_string:” che indica l'indirizzo della stringa identificata dalla direttiva “.ascii” la direttiva “.ascii” indica al compilatore che vi e' una successione di caratteri in ASCII. Nella sezione relativa al codice troviamo una direttiva “.globl” che rende visibile l'etichetta “_start” ai restanti moduli del programma o progetto. “_start:” e' l'indirizzo dove inizia il programma. Ora per le restanti linee di codice troviamo due chiamate alla libreria “C” con i relativi passaggi di parametri. La prima chiama la funzione “printf” e la seconda chiama la funzione “exit” , tramite l'istruzione “pushl” vengono passati i parametri allo stack e questi utilizzati dalla funzione in questione. Le stringhe in “C” per convenzione devono terminare con il carattere NULL “\0”. Ho scelto di partire direttamente interfacciandomi con il linguaggio “C” in quanto esiste già una moltitudine di librerie collaudate e testate, nondimeno essendo in un ambiente open source e shared kwnoledge, e' opportuno che sin dall'inizio si cerchi di utilizzare gli strumenti messi a disposizione da altri al fine di poterli migliorare per poi condividerli con la comunita'. E' possibile utilizzare anche un approccio diretto con le chiamate di sistema le “system call” che vedremo piu' avanti. Questa comunque e' solo una mia scelta nulla toglie che potete orientarvi come meglio credete. Continuando nella spiegazione ora e' la volta di linkare il codice oggetto. “-dynamic-linker /lib/ldlinux.2” permette al nostro programma di essere linkato con la libreria e la clausola “-lc” dice di linkarlo con la libreria “C” “libc.so”. Da notare, anche se tratteremo lo stack piu' avanti che dopo la chiamata alla subroutine viene ripristinata la posizione iniziale dello stack, togliendo quanti elementi aggiunti, o messi in pila prima della chiamata alla funzione. Il simbolo del dollaro '$' sta a significare per valore; se viene omesso, il compilatore fa riferimento al contenuto dell'indirizzo di memoria specificato dalla label. Ora vediamo le istruzioni in linguaggio macchina utlizzate : PUSHL - ADDL – CALL – (trattazione estesa nel capitolo : Aritmetica e Logica 2) (breve trattazione vedere : paragrafo Convezioni di chiamata) PUSH PUSH : Spinge un valore in cima allo stack. Il puntatore dello stack viene diminuito Formato FLAG : : PUSH tutti Sorgente Operandi Esempi memoria PUSHL $VAR (indirizzo della variabile) registro generale PUSHL %EAX (contenuto di EAX) memoria PUSHL MEM (contenuto della memoria) Immediato PUSHW $0 memoria PUSHW array(%esi*4) Operazione Simbolo PUSHW %AX (valore immediato) BYTE/WORD 4 1 Stack iniziale 65535 -8 0xFFFFh 65527 0xFFF7h Lo stack cresce diminuendo la dimensione della catasta, viceversa quando, estraiamo informazioni dallo stack questo aumenta. Nel programma precedente avendo inserito un long (2 word) per convenzione di chiamata occorre ripristinare tanti byte quanti se ne spingono nello stack; allora vengono addizionati al registro ESP 8 byte tale da riportare il valore al punto di partenza. Operazione Simbolo BYTE/WORD PUSHL %EAX 8 2 ADDL $8,%ESP 8 2 Stack iniziale 4.294.967.265 4.294.967.257 4.294.967.257 -8 +8 0xFFFF:FFFFh 0xFFFF:FFF7h 0xFFFF:FFFFh La mia personale intepretazione dello stack e' questa schema : indirizzo byte contenuti 0xFFFF:FFF7h * * * * 0xFFFF:FFFBh * * * * 0xFFFF:FFFFh **** cima/base In questo modo se mettiamo la cima in basso, effettivamente ad ogni operazione PUSH vediamo che lo stack si sposta verso l'alto, anche se diminuisce le dimensioni. POP POP : Estrae un valore in cima allo stack. Il puntatore dello stack viene diminuito Formato FLAG : : POP tutti Sorgente Operandi Esempi memoria POPL VAR (indirizzo della variabile) registro generale POPL %EAX (contenuto di EAX) memoria POPL MEM (contenuto della memoria) Immediato POPW $%ax (valore immediato) memoria POPW array(%esi*4) Operazione Simbolo POPW %AX BYTE/WORD 4 1 Stack iniziale 65527 +8 65535 0xFFF7h 0xFFFFh In questo caso lo stack aumenta di dimensioni estraendo il valore. Operazione Simbolo BYTE/WORD Stack iniziale POPL %EAX 8 2 4.294.967.257 4.294.967.265 +8 0xFFFF:FFF7h 0xFFFF:FFFFh SUBL $8,%ESP 8 2 4.294.967.257 -8 0xFFFF:FFF7h Come funzionano le librerie condivise Potevamo attraverso le chiamate di sistema arrivare allo stesso risultato senza l'ausilio di librerie esterne, così facendo il programma rimaneva delegato ad un solo blocco in quanto conteneva tutto quello di cui aveva bisogno. Questi programmi vengono chiamati “statically-linked executable”. Nel nostro esempio utilizzando una libreria condivisa il nostro programma e' linkato dinamicamente questo significa che non tutto il codice che serve al programma e' contenuto con esso. La direttiva -lc indica al linker di utilizzare la libreria “C ”. Quando mandiamo in esecuzione il programma “./primo” viene caricata in memoria inanzitutto la libreria /lib/ld-linux.so.2 questo e' il linker dinanico. Successivamente questo linker vede che il programma “./primo” necessita per operare della libreria C chiamata libc.so, ricerca i simboli “printf” e “exit” e quindi carica la libreria nella memoria virtuale. I simboli sono praticamente delle label e quindi quando la libreria viene linkata all'interno di “primo” questi vengono sostituiti dal corretto indirizzo in memoria” . Se noi impartiamo il seguente comando “ldd primo” possiamo vedere questo output ; debian:~/source# ldd primo libc.so.6 => /lib/tls/libc.so.6 (0x40026000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000) ldd visualizza le dipendenze relativamente alle librerie come possiamo vedere primo ha linkato dinamicamente le sopraelencate librerie. Il numero 6 e 2 indica la versione della libreria, i numeri tra parentesi non e' detto che siano uguali. Ora la questione e' dove reperire informazioni sulla libreria. Ogni libreria relativamente al codice sorgente ha i suoi header file, dove vengono specificati i prototipi delle funzioni, in questo posto possiamo vedere i parametri di cui la funzione ha bisogno. ( /usr/include/stdio.h ). ancora possiamo avere informazioni da objdump -R /lib/lib... e la libreria che ci interessa ma questo output e' piuttosto lungo e noioso ancora la fonte piu' ampia di informazioni e' sicuramente il WEB. Questi sono i prototipi di funzione definiti in : /usr/include/stdio.h extern int printf (__const char *__restrict __format, ...); /usr/include/stdlib.h extern void exit (int __status) __THROW __attribute__ ((__noreturn__)); Notiamo che la funzione exit non ha valori di ritorno come indicato void, e i punti nella printf “...” indica che puo' ricevere un numero variabile di parametri.” ora vediamo questo secondo programma File : secondo.s # # Secondo Programma .section .data output_string: .ascii “ Ciao mi chiamo %s e ho %d anni ! \n\0” nome: .asciz “Claudio Daffra” eta: .long 34 .equ .equ EXIT SYSCALL ,1 ,0x80 .section .text .globl _start _start: pushl pushl pushl call eta $nome $output_string printf addl $12,%esp movl movl $0 , $EXIT , int $SYSCALL %ebx %eax L'output produce questa stringa : Ciao mi chiamo Claudio Daffra e ho 34 anni!”. In questo caso vediamo come la stringa “output_string” contenga dei caratteri con il simbolo di percentuale, in questo caso informa la printf del numero dei parametri di cui avra' bisogno in questo caso 2, successivi all'inserimento del primo che e' la stringa stessa. I parametri in questo caso vengono inseriti in ordine inverso, tramite la tecnica LIFO , vengono prelevati nel corretto modo. Una cosa ancora molto importante, la prima istruzione delle tre “pushl” manca del simbolo del dollaro “$”, questo significa che il parametro viene passato per VALORE e non per INDIRIZZO come le altre due pushl in cui il loro scopo e' quello di passare l'indirizzo di partenza delle due stringhe alla funzione. Se mettevo questa itruzione (errore) pushl $eat, ottenevo in output l'indirizzo della variabile eta!. Notiamo l'uso di una altra direttiva importante “.long” che definisce che il numero seguente e' lungo una word e quindi riserva spazio per esso. Non dissimile e' la restante parte del programma tranne che in questo caso per terminare l'esecuzione non viene chiamata la funzione “exit” della libreria “C” ma viene fatta una chiamata di sistema. La direttiva “.equ” definisce dei simboli da sostituire con il corretto codice che viene riportato dopo la virgola. in questo caso EXIT equivale a 1 e SYSCALL equivale a 0x80. Generalmente le chiamate di sistema necessitano dei parametri contenuti nei registri e del numero della syscall che viene messa nel registro eax, in questo caso il simbolo % indica al compilatore di caricare il registro eax con il valore assoluto di 1 (movl $EXIT, %eax è uguale a eax = 1). Viene utilizzato nelle chiamate a sistema il numero 0x80h e non 80 in quanto quest'ultimo e' la sua rappresentazione in decimale (0x80 = 128 ). Come ultima cosa nel listato avrete notato la direttiva “.asciz” questa serve a mettere un carattere NULL al termine della stringa, in alcuni casi la preferisco che la prima sintassi, in quanto le operazioni in C sulle stringhe per capire la fine di essa fanno riferimento al carattere “\0”, si andrebbe incontro a errori se questo verrebbe omesso. Nella sintassi AT&T viene messo il simbolo “%” percentuale per evidenziare il nome dei registri e non confonderlo con le comuni vairabili. INT INT : Software interrupt. Esegue Pushf,pushw cs,pushl eip. Questa istruzione salva nello stack gli indicatori di posizione corrent e determina la locazione della nuova esecuzione. Formato : INT immediato Operandi Esempi istruzione INT $0x80 Linux System Call MOV MOV : sostituisce l'operando destinatario con quello sorgente. Se l'operando destinatario è costituito da un registro di segmento, l'istruzione caricherà il descrittore associato al selettore che si trova nei registri shadow x386. Verranno , a meno che il valore del selettore è 0, effettuati test e controlli di privilegio sulla legalità del descrittore. Formato FLAG : : MOV tutti srogente, destinazione Operandi Esempi memoria,registro movl %eax,mem registro,memoria movl mem,%eax regitro,registro movw %ax,%di Immediato,registro movb %1,%al memoria,resigtro POPW array(%esi*4) ,%eax memoria,memoria non possibile! File : terzo.s # codifica delle variabili in C # int c0 = 65.535 ; 32 bit # int c1 = 4.294.967.295 ; 32 bit # long c2 = 4.294.967.295 ; 32 bit # long h1 = 0x1234ABCD ; 32 bit # # float c3 = 987.98754321-E20 ; 32 bit # double c4 = 123.456789-E300 ; 64 bit # .section .data c0: .word c1: .int c2: .long h1: .long 0xffff 0xffffffff 0xffffffff 0x1234ABCD c3: .float 987.98754321E-20 c3tmp: .double 0.0 c4: .double 123.456789E-300 output_string: .asciz "\n Word %u \n Int %d \n Long %u \n Hex %x\n Float %e \n Double %g \n" .section .text .global _start _start: #................trasforma da float a double fld c3 fstp c3tmp # ...............i parametri secondo convezione di chiamata # ...............vengono messi nell'ordine inverso pushl pushl pushl pushl pushl pushl c4+4 c4 c3tmp+4 c3 h1 c2 pushl pushw pushw pushl c1 $0 # high word c0 # low word $output_string call addl printf $36,%esp pushl $0 call exit OUTPUT : debian:~/source# ./terzo Word 65535 Int -1 Long 4294967295 Hex 1234abcd Float 2.918732e-315 Double 1.23457e-298 debian:~/source# Commento : Questo semplice programma mostra come utilizzare i vari formati numerici e la loro lunghezza in memoria, alcuni concetti sembreranno ancora oscuri in quanto non fornisco una spiegazione ma l'obiettivo e' quello di mostrare solo l'utilizzo di questi numeri, poi' per quanto rigguarda lo STACK e le convenzioni di chiamata, opportuni paragrafi approfondiranno l'argomento. Oltretutto nell'output notate la distinzione tra i numeri con segno e quelli senza. La prima parte commentata e' essenzialmente una codifica in C dello stesso, la parte sopra mostra la medesima codifica in assembler a cui ho voluto aggiungere il massimo per quello spazio in memoria. Se in una word intendiamo passare un argomento maggiore di quanto possa essere ospitato nello spazio fisico il compilatore ci avvertira' con un warning che lo stesso verra' tagliato, “truncated”. Notiamo i due numeri in floating point o virgola modible float e double che (verra' fatta una trattazione piu' ampia successivamente), entrambi sono delle quad word numeri a 8 byte, le prime 2 istruzioni estendono il float a double e lo memorizzano in una seconda variabile temporanea “c3tmp”. Ho inizializzato la variabile a 0.0 se proviamo a mettere solo 0 il compilatore ci avvertira' con un errore. Come potete vedere dal listato i parametri sono passati in ordine inverso questa e' una convenzione di chiamata del compilatore C oltretutto essendo gestiti come Quad Word, spingo nello stack i due long adiacenti in memoria, prima la parte bassa e poi quella alta. E cosi la medesima cosa la faccio per la variabile C4. Ancora nel listato ho la codifica di 2 numeri “long” che non mi creano particolari problemi ad eccezione del numero intero che in stampa equivale a -1. Questo viene trattato com “signed decimal” dal suffisso C “%d”. Una particolare attenzione sta nel passare le word alla subroutine, dato che la funzione C richiede un numero long spingero' nello stack la parte più bassa cio' il numero effettivo contenuto nella word tramite una pushw c0 e poi aggiungo la parte alta con degli zero “pushw $0”. Ho tralasciato la spiegazione della visualizzazione del numero in esadecimale in quanto non comporta particolare interesse. Tabella spazi in memoria e simboli compilatore GAS. simbolo byte % in printf .long .int .word .hword 2 .byte .double .float .tfloat 4 4 2 %d %u 1 8 8 10 %d %u %x %d %u %x %d %u %x %u %c %E %g %e %g %e %g CALL CALL : Tramite questa istruzione e' possibile gestire le subroutine all'interno di un programma; momentaneamente l'esecuzione e' passata alla funzione specificata dall'indirizzo di call e quando questa ha termine, cioè quando incontra una istruzione RET, continua a eseguire l'istruzione successiva alla call. Quando viene eseguita, l'istruzione CALL spinge in cima allo stack gli indirizzi di ritorno, i quali vengono correttamente caricati in memoria dall'istruzione RET, per assicurarsi che l'esecuzione del programma continui all'istruzione successiva dopo la call in questione*. L'istruzione call può anche speficicare anche un altro segmento di memoria. (ES,FS,GS). Per default viene utilizzato DS. FORMATO FLAG : : call indirizzo tutti Operandi Esempi memoria call indirizzo registro call eax registro call (eax) memoria call es:indirizzo memoria call fs:indirizzo (ds:indirizzo default) Esempi : istruzione Cosa esegue a livello di stack call printf push CS push EIP Possiamo identificare 2 tipi di call, a seconda che la destinazione sia all'interno del segmento o in un segmento diverso. CALL FAR PROCEDURE - push – push CS eip CALL NEAR PROCEDURE – push EIP * Molte debolezze dei sistemi avvengono proprio in questo punto; con la tecnica di “buffer overflow”, in questo caso aggiungendo dati al buffer riusciamo a forzare lo stack in modo tale che l'indirizzo di ritorno punti ad una nostra routine (injection code). Istruzioni di ritorno RET RET = questa istruzione ripristina, il puntatore delle istruzioni al valore che aveva prima dll'ultima istruzione call, puntanto all'istruzione successiva. viene estratto dallo stack il valore EIP. FORMATO FLAG : : ret tutti Operandi nessuno operando Esempi ret Alla volta del NASM Inserite questo file e salvatelo con come quinto.s. E' diversa la sintassi come potete vedere e presto al termine della propria esecuzione ve ne darò ragguaglio. file : quinto.s extern printf extern exit segment .data car1 str1 inta1 hexa1 flt1 flt2 flttmp db db dd dd dd dq dq 'a' " stringa 1 ",0 1234567 0x1234abcd 5.327e-30 -123.456789e300 0.0 format db 10," 1) %c ",10, " 2) %s ",10," 3) %d ",10," 4) %x ",10," 5) %e ",10," 6) %E ",10,0 segment .text global main main: fld dword [flt1] fstp qword [flttmp] push push push push dword dword dword dword [flt1+4] [flt1] [flttmp+4] [flttmp] push push push push push dword dword dword dword dword [hexa1] [inta1] str1 [car1] format call printf add esp,36 push call dword 0 exit Terminata la digitazione ed il salvataggio occorre compilare il programma in questione tramite questi comandi : 1) nasm -f elf quinto.s 2) gcc -o quinto quinto.o 3) ./quinto Come dicevamo, sostanzialmente “terzo.s” il programma scritto per il compilatore GAS e il quinto.s programma scritto per il compilatore NASM producono un risultato abbastanza analogo, ossia mostrano come utilizzare alcuni formati numerici in relazione ad un utilizzo con il compilatore C. Tuttavia la sintassi e' molto differente, come potete notare il compilatore utilzza la sintassi AT&T questo indica che l'uguaglianza va da sinistra a destra, se prendo in considerazione l'istruzione MOVL $0,%EAX questa e' simile a EAX = 0, in riferimento al compilaqtore NASM che usa la sintassi INTEL la medesima andrà scritta diversamente MOV EAX , word 0 cioe' EAX = 0. La scelta dell'uno o dell'altro e' piuttosto una comodita'. Il nasm specifica il formato in cui viene generato il codice oggetto in questo caso il formato e' ELF . Non viene linkato con LD piuttosto viene utilizzato il compilatore gcc per generare l'eseguibile. Ultima cosa mentre nell'etichetta globl definisco il simbolo _start come punto di ingresso, il C tramite il compilare GCC necessita di main come punto di ingresso principale dei programmi. Altre particolarità visive nel listato sono l'utilizzo delle parentesi quadre, per comunicare di utilizzare il valore in memoria identificato dall'indirizzo e l'utilizzo della clausola extern per indicare che tali funzioni non sono presenti ma verrano linkate dinamicamente. Ultima piccola cuiriosità non e' possibile utilizzare i simboli '\n' i caratteri speciali all'interno delle stringhe, vengono comunque utilizzati dei numeri per identificare “\n\r\0” = 10,13,0. CAPITOLO 4 Aritmetica e Logica 1 Aritmetica e Logica #1 Forse e' uno degli argomenti piu' tediosi della programmazione, tuttavia tramite esempi pratici e opcode cercherò di annoiarvi il meno possibile. Anche se la possibilità offerta da tali operatori non e' da sottovalutare. Verrà fornita prima una disserzione sulla teoria e poi visionerete direttamente le istruzioni x86 per manipolare queste operazioni con i relativi esempi per meglio chiarirvi(mi) il concetto. Spero possa esservi utile. Rappresentazione numerica nel compilatore GAS Il compilatore GAS supporta alcuni formati numerici : Binario, Ottale, Decimale e Esadecimale Float tramite simboli e numeri veri e propri. Di seguito mostro una breve tabella con alcuni simboli e alcuni parametri di inizializzazione .byte .ascii .octa .float .long .word ... 74,0112,092,0x4A,`J.`\J # è l'accento rovesciato “suona la campana\7” 0x12345FF 0F-3141.69339E-40 # pi greco ... Tabella in relazione alla'quantit' di bye che ogni simbolo può contenere BYTE .byte WORD DWORD .word .int .short .long QWORD Ten byte .quad .hword .float .double .tfloat Ora vediamo come mettere i vari sistemi numerici : binario ottale decimale esadecimale 0b o 0B seguito dal numero--> 0 poi il numero --> qualsiasi numero --> 0x 0X e numeroi --> es. es. es. es. 0b00110011 / 0117773712 12384614 0xFF34ED12 / 0B00110011 0XFF34ED12 Questo ci torna molto utile nella nostra discussione sulle operazioni logiche, in quanto opererò sui bit quindi risulta difficile se ogni volta devo codificare il risultato in altri sistemi quali decimali o esadecimali pe introdurre il numero. Mentre e' molto più semplice anche da un punto di vista del debugging poter utilizzare i numeri direttamente al loro scopo. Mi spiego meglio utilizzando le operazioni logiche e lavorando sui bit : quando dovrò introdurre la maschera per ottenere il risultato desiderato, farò riferimento nell'immediatezza alla numerazione binaria, se antepongo a questa quella esadecimale ad occhio il risultato non e' chiaro. Cosi nell'utilizzo delle operazioni in floating point non utilizzero' l'esadecimale. Quando dovro' affrontare alcuni operazioni che lavorano su i 12 bit utilizzero' l'ottale. Operazioni Logiche In riferimento ai numeri binari ci sono alcune operazioni logiche (Logica Booleana) che sono alla base del funzionamento del computer e dei circuiti integrati. Ci sono 4 principali operazioni logiche piu' qualche altra degna di nota : AND OR XOR and NOT. AND Significa “e” e tramite la tabella di verita' otteniamo lo stesso risultato solo se i due numeri sono 1 AND 0 1 0 0 0 1 0 1 Un importante utilizzo dell'operazione AND e' di poter forzare un risultato di un bit a zero, in effetti qualsiasi numero binario in concomitanza con AND zero e' sempre 0. Contrariamente se prendiamo qualsiasi bit e facciomo un AND 1 otteniamo sempre il bit di partenza. OR la logica di questo operatore e' differente dal primo, OR 0 1 0 0 1 1 1 1 Utilizzero' OR quando dovro' settare a 1 un qualsiasi bit. Un piccolo trucco possiamo vederlo nella rappresentazione dei caratteri ASCII da minuscolo a maiuscolo, che basta fare un OR in una data posizione che questo diventa da minuscolo a maiuscolo! XOR Significa Exclusive OR, per capire questa logica facciamo riferimento a questo esempio, se andiamo al supermercato a fare spese spendiamo dei soldi, se non ci andiamo i soldi ci restano; cosi' funziona XOR non possiamo aver l'uno e l'altro , cioe' due numeri binari identici danno sempre 0. XOR 0 1 0 0 1 1 1 0 In effetti se spendo i soldi non li ho piu' non posso sperare di avere sia la spesa che i soldi. A meno che non ci sia un cortocirtuicto nella macchinetta delle bibite che mi restiuisce il gettone e mi da la lattina :-> ! NOT NOT 0 1 NOT 1 0 qui la situazione viene capovolta, invertita. In precedenza ho detto che il processore x86 può indirizzare come minima quantità il byte quindi un minimo di 8 bit alla volta. poi a seconda dei registri questi posso indirizzare alla volta 16, 32 o 64 bit. Le operazioni logiche in un'architettura x86 operano bit-by-bit (bitwise). Se vogliamo computare l'operazione logica AND tra questi due byte F4h e 19h, otteniamo questo risultato F4h 244 11110100b AND 19h 25 00011001b ---------------------------10h 16 00010000b come potete vedere il risultato e' corretto rimane a 1 un solo bit. Ci sono poi altre operazioni in cui risulta importante l'utilizzo degli operatori booleani, per esempio se in un byte vogliamo ottenere il nibble basso faremo un AND 00001111h e la parte alta dell'ottetto verra' azzerata, potrebbe servire questo anche per i numeri BCD packed. Una volta visto la parte teorica vediamo come metterla in pratica attraverso le istruzioni in linguaggio macchina. Corrispondenza tra circuiti elettronici e funzioni booleane La corrispondeza tra circuiti elettonici e operazioni booleana è di 1:1. Quindi per ogni operazione logica è possibile disegna un circuito elettronico. Unitamente alle canoniche 3 operazioni logiche ne aggiungiamo una quarta : NAND, che non fa altro che applicare un NOT al risultato dell'operazione AND tra i due operandi. schema : AND OR A--->| |--> a AND b B--->| A→| B→| NOT |→ a OR a a → ° ! a NAND A→| B→| | →° a NAND b Tramite 2 circuiti NAND in sequenza costruiamo un circuito AND. Così via otteniamo la base per costruire l'architettura di un elaboratore. Schemi circuiti elettrohici : NOT : AND : OR : XOR : XOR ottenuto da circuiti elementari : Istruzioni macchina logiche Come mostrato precedentemente attraverso le tabelle possiamo effettuare delle operazioni logiche, l'equivalente delle istruzioni in linguaggio assembler sono : – – – – AND OR XOR NOT Negazione NOT = modifica l'operando iniziale applicando ad ognuno dei suoi bit l'operazione logica NOT. Formato flag : : NOT destinazione non modifica i flag Operandi Esempi Memoria NOTW var Registro Generale NOTL %ECX Supponiamo di voler inverire tutti i bit di un operando in %al %al 01010101 destinatario NOTB %AL %al 10101010 risultato Prodotto logico AND = Sostituisce ciascun bit dell'operando di destinazione con il risultato dell'operazione logica Formato Flag : : AND sorgente,destinazione Modifica tutti i flag. Operandi Esempi registro generale,memoria ANDL %EDX , MEM memoria,registro generale ANDB MEM , %CL registro gen, reg generale ANDW %CX,%DX Immediato,Memoria ANDB $0X7C , ADDRESS Registro generale, Immediato ANDL %EBX , $0X45FFA4F0 supponiamo di avere 8 bit nel registro %AL e ci interessano solo il bit 5 e 0 tutti gli altri andranno ripuliti, quindi occorrera' impostare come maschera 00100001b, ed eseguire l'operazione : %al = mask = 01100110 00100001 destinatario sorgente ANDB 0b00100001 , %al %al = 01000010b risultato Somma Logica OR = sostituisce a ciascun bit dell'operando di destinazione il risultato dell'operazione OR. Formato flag : : OR sorgente,destinazione modifica tutti i flag, azzera CF ed OF Operandi Esempi registro generale,memoria ORL %EDX , MEM memoria,registro generale ORB MEM , %CL registro gen,registro generale ORW %CX,%DX Immediato,memoria ORB $0X7C , MEM Registro generale,Immediato ORL %EBX, MEM supponiamo di avere 8 bit nel registro %AL e ci interessano solo il bit 5 e 0 tutti gli altri considerati privi di significato e vadano forzati a 1, quindi occorrera' impostare come maschera 00100001b, ed eseguire l'operazione : %al = mask = 01010011b 01100110b destinatario sorgente ANDB 0b01100110 , %al %al = 01110111b risultato OR Esclusivo XOR = sostituisce ciascun bit dell'operando destinatario con il bit relativo all'operazione XOR. Formato Flag : : XOR sorgente , destinazione modifica tutti i flag. Operandi Esempi registro generale,memoria XORL %EDX , mem memoria, registro generale XORB mem , %CL registro gen , regitro gen XORW %CX,%DX Memoria, Immediato XORB $0x7C , address Immediato,reg generale XORL $0x45FFA4F0 , %EBX Supponiamo di avere un ottetto nel registro %AL e di voler complementare il bit 7 e 1 e lasciare inalterati tutti gli altri. %al 01010011b mask 01100110b destinatario sorgente XORB 0b01100110 , %AL %al 00110101b risultato Test TEST = equivale all'istruzione AND, tuttavia non modifica l'operando di destinazione, ma si limita solo a modificare i flag. Formato Flag : : TEST sorgente,destinazione modifica il contenuto di tutti i flag. Operandi Esempi Memoria registro generale TESTL mem , %EDX Registro generale, memoria TESTB %CL , mem Reg. gen , Reg gen. TESTW %CX,%DX Immediato,Memoria TESTB $0x7C , mem Immediato,reg. generale TESTL $0x45FFA4F0 , %EBX Supponiamo di avere un operando a 8 bit nel registro %AL, e ci interessa verificare se il bit 5 vale 0 oppure 1. Per fare questa operazione ci occorre l'istruzione TESTB fissando come operando destinatario il registro %AL e come operando sorgente la maschera pari 00100000b : TESTB 0b00100000 , %AL Dopo questa operazione il Flag ZF ci dice se il risultato dell'operazione e' stato globalmendo zero (ZF=1) oppure no (ZF=0). quindi se : ZF=1 il bit 5° di %AL ZF=0 il bit 5° di %AL era era 0 1 BT BT = Bit Test.Questa istruzione identifica correttamente il bit della sorgente, specificato dall'indice e lo copia poi nel flag del carry Formato Flag : : BIT INDICE , SORGENTE Copia il Bit interessano nel Carry Operandi Esempi BITB $0,%al indice 0 %al=0b00001101 carry = 1 BITB $1,%al indice 1 %al=0b00001101 carry = 0 BITW $1,%ax $1 BITL mem,%EAX dipende dall'indice in memoria da 0 a 31 %ax=0b00000000:00001101 carry = 0 BTC BT = Bit Test and complementa .Questa istruzione identifica correttamente il bit della sorgente, specificato dall'indice e lo copia poi nel flag del carry, complementando il bit sorgente (eseguendo l'operazione NOT) Formato Flag : : BIT INDICE , SORGENTE Copia il Bit interessano nel Carry Operandi Esempi BITB $0,%al indice 0 %al=0b00001101 carry = 1 %al=0b00001100 BITB $1,%al indice 1 %al=0b00001101 carry = 0 %al=0b00001100 BITW $1,%ax $1 %ax=0b0000:0000:0000:1101 carry=0 %ax=0b0000:0000:0000:1111 BITL mem,%EAX dipende dall'indice in memoria da 0 a 31 BTR BT = Bit Test and reset .Questa istruzione identifica correttamente il bit della sorgente, specificato dall'indice e lo copia poi nel flag del carry, resettandolo=impostando a zero il bit sorgente . Formato Flag : : BIT INDICE , SORGENTE Copia il Bit interessano nel Carry Operandi Esempi BITB $0,%al indice 0 %al=0b00001101 carry = 1 %al=0b00001100 BITB $1,%al indice 1 %al=0b00001101 carry = 0 %al=0b00001100 BITW $1,%ax $1 %ax=0b0000:0000:0000:1101 carry = 0 %ax=0b0000:0000:0000:1101 BITL mem,%EAX dipende dall'indice in memoria da 0 a 31 BTS BT = Bit Test and set .Questa istruzione identifica correttamente il bit della sorgente, specificato dall'indice e lo copia poi nel flag del carry, settandolo=impostando a uno il bit sorgente . Formato Flag : : BIT INDICE , SORGENTE Copia il Bit interessano nel Carry Operandi Esempi BITB $0,%al indice 0 %al=0b00001101 carry = 1 %al=0b00001101 BITB $1,%al indice 1 %al=0b00001101 carry = 0 %al=0b00001101 BITW $1,%ax $1 %ax=0b0000:0000:0000:1101 carry = 0 %ax=0b0000:0000:0000:1111 BITL mem,%EAX dipende dall'indice in memoria da 0 a 31 Operazioni di Rotazione Un' altro set di operazioni logiche su cui lavorare sono quelle di rotazione. Troviamo istruzioni per traslare a destra o a sinistra i i bit in una stringa di bit. 76543210 -------←01010101 rotazione verso sinistra 76543210 -------10101010 Come potete vedere ruotando i bit vengono ruotati verso sinistra di una posizione e nel bit 0 viene messo lo zero. Dobbiamo chiederci a questo punto una cosa, Dove va a finire il bit 7 e soprattutto cosa entra nel bit 0 ? La risposta e' che per quanto riguarda il bit 0 dipende dal contesto. Il bit 7 va a finire fuori, CARRY OUT. Da notare che lo scorrimento a sinistra nelle operazioni logiche equivale a moltiplicare il numero per la sua radice, in questo caso 2 che e' un numero binario. Il numero precedente e' 85 allora mi aspetto che dopo una rotazione ottenga questo numero 85*2 cioè 170; in effetti ottengo come risultato 1010:1010b esattamente 170. Quindi se ruotiamo un numero verso sinistra di n volte allora moltiplicherò il numero per n volte. Per esempio prendiamo il numero 10 (0000:1010) se scorro questo ottetto di 3 volte verso sinistra, otterrò 10 partenza 20 , 40, 80 vediamo 76543210 -------3←00001010 ruoto a sinistra per 3 volte 76543210 -------01010000 80 decimale Ruotando invece i bit verso destra accade l'operazione opposta, il numero viene diviso per la sua potenza 148 decimale 37 decimale 76543210 -------10010100 →2 ruota a destra 2 volte 76543210 -------00100101 Le rotazioni dei bit appena descritte si applicano ai numeri positivi (unsigned byte), in quanto se trattiamo un numero negativo e operiamo un scorrimento verso destra di 1 otteremo un valore scorretto. -2 decimale 1000:0001b 65 decimale 76543210 -------10000010 →1 ruota di uno -1 decimale = 76543210 -------01000001 Questo accade perchè il bit 7 prima conteneva 1 correttamente per i numeri negativi ora contiene 0 quindi e' passato a essere un numero positivo. Quindi per poter effettuare questa operazione dovremo garantire che il bit 7 rimanga nel segno 1. -2 decimale 129 decimale -1 decimale = 1000:0001b 76543210 -------10000010 →1 ruota di uno 76543210 -------10000001 in questo caso corretto. SHR SHR = Traslazione logica verso destra; interpreta l'operando come numero naturale e trasla il contenuto tante volte quanto indicato dall'operando sorgente.Sostituisce con zero il bit più significativo dell'operando destinazione. pone a zero il flag di overflow. formato flag 0 -> : : SHR sorgente,destinazione modifica tutti, CF 76543210 -> CF Operandi Esempi immediato,memoria SHRB $7 , MEM registro CL,memoria SHRW %CL , MEM immediato registro generale SHRL $1 registro CL, reg. gen. SHRW %CL , %BX , %EAX Guardando all'operando destinatario questo istruzione divide il numero per 2 e lo sostituisce con il quoziente approssimato per difetto. SAR SAR = traslazione aritmetica verso destra, interpreta l'operando sorgente come se fosse un numero naturale positivo/negativo e per n volte sostituisce il CF con il bit 0 e mantiene il segno del bit 7. formato flag : : SAR tutti sorgente,destinazione , OF=0 7 --> 76543210 --> CF Operandi Esempi immediato,memoria SARB $7 , MEM registro CL,memoria SARW %CL, MEM immediato registro generale SARL $1 ,%EAX registro generale, CL SARW %CL,%BX Guardando all'operando destinatario questo istruzione divide il numero per 2 e lo sostituisce con il quoziente approssimato per difetto. SHL SHL = Traslazione logica verso sinistra; interpreta l'operando come numero positivo, trasla il contenuto tante volte quanto indicato nell'operando sorgente. formato flag CF <-- : : SHL sorgente,destinazione modifica tutti, CF,OF =0 76543210 <-- 0 Operandi Esempi immediato,memoria SHLB $7 , MEM registro CL,memoria SHLW %CL , MEM immediato registro generale SHLL $1 CL,registro generale SHLW %CL , %BX , %EAX Guardando l'operando destinatario questo istruzione moltiplica il numero per 2. SAL SAR = traslazione aritmetica verso sinistra, interpreta l'operando destinatario come se fosse un numero naturale positivo/negativo e per n volte sostituisce il CF con il bit 0. Modifica il valore del 7 bit con il 6 e qualora questo venga cambiato, attiva a 1 il flag overflow. formato flag : : SAR srogente,destinazione tutti, OF=0 CF <--76543210 <-- 0 Operandi Esempi immediato,memoria SARB $7 , VAR registro CL,memoria SARW %CL , MEM immediato registro generale SARL $1 registro generale, CL SARW %CL , %BX , %EAX ROL ROL = rotazione a sinistra : interpreta l'operando sorgente come un numero naturale e per n volte trasla i bit dal precedente al successivo e dal bit 7 a quello 0. il bit 7 va a finire anche nel CF Formato Flag : : CF 76543210 <-- 7 <-- ROL sorgente , destinazione modifica CF,OF Operandi Esempi immediato,memoria ROLB $7 , MEM registro CL,memoria ROLW %CL , MEM immediato registro generale ROLL $1 CL,registro generale. ROLW %CL , %BX , %EAX ROR ROL = rotazione a destra : interpreta l'operando sorgente come un numero naturale e per n volte tasla i bit da successivo al precedente e quello in 0 al 7 bit. il bit in 0 va anche nel CF Formato Flag : : ROR sorgente , destinazione modifica CF,OF 0 --> 76543210 0 --> CF Operandi Esempi immediato,memoria RORB $7 , MEM registro CL,memoria RORW %CL, MEM immediato registro generale RORL $1 , %EAX CL,registro generale. RORW %CL,%BX RCL RCL = rotazione a sinistra attraverso il carry : interpreta l'operando sorgente come un numero naturale e per n volte trasla i bit dal precedente al successivo e dal bit 7 a CF. il CF va nel bit 0. Formato Flag : : CF 76543210 <-- CF <-- ROL sorgente , destinazione modifica CF,OF Operandi Esempi immediato,memoria RCLB $7 , MEM registro CL,memoria RCLW %CL, MEM immediato registro generale RCLL $1 , %EAX CL,registro generale. RCLW %CL, %BX RCR RCR = rotazione a destra attraverso il carry : interpreta l'operando sorgente come un numero naturale e per n volte tasla i bit da successivo al precedente e quello in 0 al CF e dal CF al 7. Formato Flag CF --> : : ROR sorgente , destinazione modifica CF,OF 76543210 --> CF Operandi Esempi immediato,memoria RCRB $7 , MEM registro CL,memoria RCRW %CL, MEM immediato registro generale RCRL $1 , %EAX CL,registro generale. RCRW %CL, %BX Dato che la parte aritmetica e logica e' piuttosto corposa, preferisco qua e là staccare con alcuni brevi esempi e introducendo ogni qual volta pochi concetti nuovi, riguardo il compilatore GAS e le librerie C. Ritengo che per le restanti istruzioni cui non fornisco direttamente un esempio, sia il lettore a sperimentare direttamente sul campo; personalmente dopo un paio di “segmentation fault” in linux sono arrivato al risultato corretto, di molti programmi; alcuni erano errori banali, altri erano dimenticanze anche del simbolo $ che cambiavano il contesto. Digitate questo programma : file : settimo.s # immetti un valore decimale e moltiplicalo .section .bss var1: .long 96 .section .data output_string1: .asciz "\n Introduci un numero : " format: .asciz "%d" output_string2: .asciz "\n il numero è : %d \n" output_string3: .asciz "\n il doppio e' : %d \n " .section .text .global _start _start: # ...............i parametri secondo convezione di chiamata # ...............vengono messi nell'ordine inverso pushl $output_string1 call puts addl $4,%esp pushl pushl call addl $var1 $format scanf $8,%esp # &var pushl pushl call addl var1 $output_string2 printf $8,%esp movl shll var1,%eax $1,%eax push pushl call addl %eax $output_string3 printf $8,%esp # eax = var1 pushl $0 call exit Ho introdotto pochi concetti nuovi. Come vedete nella parte piu' in alto compare una nuova section la .BSS . Questa viene utilizzata quando si vuole allocare memoria dopo che il programma e' stato caricato in memoria e non direttamente nell'eseguibile come accade nella section DATA . Se vogliamo definire un array di 500.000 elementi, se non utilizzo la sezione BSS l'eseguibile sarà piu' grande di 500.000 byte o altro che utilizzo per rappresentare tale lunghezza. PUTS e' una funzione della libreria C standard e richiede solo un parametro che e' la stringa da visualizzare. SCANF è la funzione C della libreria standard che permette di introdurre, un dato da tastiera come stringhe, floating point, interi. Questa funzione richiede un input formattato per identificare il tipo di parametro che dovra' gestire come input, e l'indirizzo dovre andrà a mettere il valore immesso. In effetti a differenza della printf che richiedeva “var1” questa richiede $var1, appunto il simbolo del dollaro davanti ad una variabile sta ad indicare l'indirizzo in memoria della stessa, o un riferimento immediato della memoria. Di seguito alcuni esempi di utilizzo di scanf. Esempi : scanf (“%d”) scanf (“%g”) scanf (“%s”) decimale, long, interi floating point stringhe di testo GAS var1 oppure (var1) $var1 : : contenuto della memoria, indirizzo della memoria, VALORE ; REFERENZA ; Notate l'utilizzo dell'istruzione SHLL, scorrimento logico a sinistra di 1 posizione che moltiplica il regitro destinatario per due. MODI DI INDIRIZZAMENTO Questo e' un capitolo molto importante della programmazione in linguaggio macchina, uno dei capitoli fondamentali. Illustro tramite GAS le varie tecniche usate per indirizzare la memoria. Il processore x86 dispone di 5 indirizzamentei di memoria predefiniti quali : - Indirizzamente di memoria immediato ; – Indirizzamento di memoria diretto ; – Indirizzamento di memoria indicizzato ; – Indirizzamento di memoria indiretto ; – Indirizzamento di memoria Puntatore-Base ; – Indirizzamento tramite rigistri ; INDIRIZZAMENTO IMMEDIATO Questo tipo di indirizzamento e' molto semplice viene utilizzato per caricare direttamente nel registro o in memoria un valore desiderato : Esempi : Istruzioni Registro Codifica C MOVL $12 , MOVL $VAR , %EAX %EAX EAX = 12 EAX = &VAR # # GAS utilizza questo indirizzamento utilizzando il simbolo DOLLARO “$”, non dimenticate questo o utilizzerete un altro modo di caricamento dati. INDIRIZZAMENTO DIRETTO Questo tipo di indirizzamento carica un valore, dalla memoria, tramite indirizzo. Esempi : Istruzioni MOVL indirizzo MOVL (indirizzo) , , Registro Codifica C %EAX %EAX EAX = *indirizzo EAX = *indirizzo INDIRIZZAMENTO INDIRETTO Questo tipo di indirizzamento carica un valore da un registro che contiene un indirizzo di memoria. A seconda del tipo di registri utlizzato si suddivide in due categorie principali. Esempi : Istruzioni Registro Codifica C MOVL MOVL MOVB MOVB %eax (%eax) %al %al %eax = *ebx ERRORE : troppe referenze !!! (%ebx) , (%ebx) , cs:(%edi), ss:(%esi), INDIRIZZAMENTO INDIRETTO BASICIZZATO : MOVB (%ebx), %al MOVB (%ebp), $al INDIRIZZAMENTO INDIRETTO INDICIZZATO : MOVB (%edi) , MOVB (%esi) , %al $al INDIRIZZAMENTO Puntatore-Base Questo tipo di indirizzamento e' simile al precedente, INDIRETTO; eccetto che addiziona un valore costante all'indirizzo del registro. ESEMPI : Istruzioni Registro Codifica / Descrizione MOVL 4(%eax), PUSHL VAR(%eax) %ebx %ebx = * ( &eax + 4 ) PUSHL *($var+%eax) INDIRIZZAMENTO INDICIZZATO Questo indirizzamento utilizza l'indirizzo e l'indico; Il formato generale per comporre l'indirizzo e' questo : Indirizzo Finale Indirizzo Finale – – – – Indice Moltiplicatore Base Offset Indirizzo Offset = = Indirizzo Offset ( %BASE_Offset , Moltiplicatore , %INDICE ) ; Indirizzo Offset + %BASE_offest + Moltiplicatore * %INDICE ; qualsiasi registro di uso generale ( %eax,%ebx,%ecx,%edx, %esi,%edi... ) ; si puo' utilizzare solo una costante ( 1 , 2 , 4) ( byte, word, dword ) ; qualsiasi registro di uso generale ( %ebp, %ebx... ) ; Indirizzo di partenza ; Esempi : Istruzioni Registro Codifica MOVL MOVL PUSHL START(,%ECX,1) ,%EAX START(%EBX,%EDI,2) ,%ECX START(%EBP,%ESI,4) %EAX = * ( START + %ECX * 1 ) %ECX = * ( START + %EBX + %EDI*2 ) Registri generali : EAX EBX ECX EDX ESI EDI, tutti con offset. per default nel segmento dati DS Registri generali : EBP ESP (per default nel segmento di stack SS). Questo tipo di indirizzamento viene usato spesso, per accedere ai vari elementi nello stack o a array o matrici. Indirizzo Finale = Base + fattore * indice + constante Registr Base Fattore Registro Indice Constante EAX 1 EAX $ EBX 2 EBX ECX 4 ECX EDX 8 EDX EBP EBP ESP // ESI EDI EDI ESI INDIRIZZAMENTO TRAMITE REGISTRI Ultimo non meno importate tipo di indirizzamento è quello, che viene fatto tramite i registri, il più veloce in assoluto!. Esempi : Istruzioni Registro Interpretazione MOVB MOVL MOVW %ESI MOVB %AL , %BL %BL = %AL %EAX , %EBP %EBP = %EAX % EDI %EDI = %ESI %AL , %AH %AH = %AL MOVL %SS:(%EBP),%EAX MOVL %GS:(%EBP),%EAX MOVL (%EBP),%EAX MOVL (%ESP),%EAX # per default SS: # per default SS: CAPITOLO 5 Aritmetica e Logica 2 Aritmetica e Logica 2 Analizziamo ora alcune istruzioni matematiche di uso comune, con qualche riferimento ai floating point.Nella vita di tutti i giorni, mentalmente eseguiamo diverse operazioni di somma e sottrazione, in riferimento a quando si va a far la spesa al super market e quando scende il nostro conto corrente, sono comuni nella vita di tutti giorni che non ci facciamo più caso. Tra l'altro non utilizziamo più le operazioni stesse come abbiamo imparato a scuola, ma tramite le più moderne calcolatrici e computer. Così nella programmazione per quanto rigarda il linguaggio assembler ci imbatteremo molto in queste istruzioni, sia da un punto di vista prettamente matematico utilizzandole nei nostri calcoli, o per aumnentare o diminuire indici di matrici o strutture.L'x86 utilizza i propri registri per arrivare al risultato. Le operazioni matematiche più comuni sono quattro : – – – – Addizione ; Sottrazione ; Moltiplicazione ; Divisione. Questo capitolo sembra piuttosto ovvio, ed in effetti nelle prime pagine lo e' tuttavia viene considerato il fatto di superare la capacit' dei registrI, a 32 bit fino ad arrivare a operazioni a 128. bit! Bisogna tenere anche in considerazione dei numeri positivi e negativi, qui la situazione si complica di pochino e del riporto di alcuni flag in alcune operazioni di somma o sottrazione, per ottenere un corretto risultato. ADDIZIONE / SOTTRAZIONE Questa operazione matemetica, scusate l'ovvietà, serve per sommare due numerI per esempio in esadecimale 0x09h + 0x01h = 0x0A e (non 0x10h). Come accennato in precedenza l'x86 compie queste operazioni all'interno dei suoi registri, quindi puo' indirizzare numeri da 8, 16 e 32 bit (ad oggi 64bit con in nuovi processori), quindi può gestire in ordine 256 65535 e 4 miliardi ... questi sono i numeri. Se la somma di due numeri non può essere contenuta in un registro, il Flag di Overflow verrà impostato a 1, e verrà impostato a 1 il Carry Flag. Così ma in modo opposto si comporta la sottrazione ADD ADD : questa istruzione somma i due operandi la notazione e'quella di AT&T da sinistra verso destra Formato Flag : : ADD tutti sorgente , destinazione Operandi Esempi immediato,memoria addl $1,address #ERRORE immediato registro generale addb $1,%ah reg. gen., reg. gen. addb %ebx,%eax # %eax += %ebx mem,registro generale. addl mem,%eax # somma 1 al registro ah # somma ad eax il contenuto della memoria SUB SUB : sottrare al conenuto dell'operando di destinazione quello sorgente. Formato Flag : : SUB tutti sorgente , destinazione Operandi Esempi immediato,memoria subl $1,address # ERRORE immediato registro generale subb $1,%ah reg. gen., reg. gen. subb %ebx,%eax # %eax -= %ebx mem,registro generale. subl mem,%eax # sottrae 1 al registro ah # sottrare ad eax il contenuto della memoria P.S. Ricordo che il suffisso (b, w, l) dopo l'istruzione sta ad indicare l'effettivo numero di byte trasferiti. MOLTIPLICAZIONE E DIVISIONE Analogamente alle prime queste sono operazioni aritmetiche di utilizzo comune, tuttavia mentre per le precedenti il risultato veniva messo nel registro di destinazione, qui vengono coinvolti dei registr addizionali per il risultato, per contenere la moltiplicazione degli operandi o per mantenere il resto dell'operazione. MUL MUL : questa e' l'istruzione assembly della moltiplicazione. Moltiplicando due numeri a 32 bit il risultato e' a 64 bit. Quindi x86 ha bisogno di 2 registri per contenere il risultato. I bit alti vengono memorizzati in EDX, quelli bassi in EAX. Il registro che x86 considera sempre come moltiplicando e' EAX solo questo. e come risultato sempre la coppia EDX:EAX: sintassi moltiplicando moltiplicatore Risultato : : : : MUL reg EAX reg,mem EAX:EDX EAX REG EDX : EAX 0x1234h * 0x1000h = 0x1234:0000h molticplicatore implcito al ax eax moltiplicatore risultato byte word dword ax dx:ax edx:eax Operandi Esempi immediato mull $2 # ERRATO reg. gen. mull %ebx memoria mull mem # eax *= *mem registro mulb cl DIV DIV : istruzione assembly per dividere. Analogamente a MUL, utilizza due registri in partenza, EDX:EAX che contengono il numero da dividere, per ottenere il risultato EAX e il resto EDX. sintassi dividendo divisiore Risultato : : : : DIV reg EDX:EAX reg,mem EAX resto EDX divisore implicito divisore quoziente resto ax dx:ax edx:eax byte word dword al ax eax ah dx edx Operandi Esempi immediato divl $2 # ERRATO registro divl %ebx # edx:eax / ebx = eax resto edx registro divw %bx # dx:ax / bx = ax resto dx registro divb %cl # ax / bl = al resto ah DX:AX / 0x1234:ABCDh / reg,mem 0x1:0000h = = AX 0x1234h DX 0xABCDh Ulteriori esempi addizione : %al 0x20 h %bl Operazione risultato carry descrizione 0x30h addb %bl,%al 0x50h / operazione add senza problemi 0xf0h 0x30h addb %bl,%al 0x20h si risultato troppo grande per essere contenuto 0x30 h si 0xfe era negativo, feh=254 + 30h=48 = 12E 0xfeh addb %bl,%al 0x2eh n.b. : i numeri sono sempre positivi ! unsigned !, e' poi come li intendiamo e gestiamo che cambiano il significato ! ( vedesi capitolo : tutto sui flags! ) Ulteriori esempi sottrazione : %al %bl Operazione risultato carry descrizione 0x20h 0x30h subb %bl,%al 0x10h / operazione SUB senza problemi 0x30h 0x33h subb %bl,%al 0xfdh si risulato -3 . gestisce correttamente ?! fd=253 0x30h 0xfeh si simile al precedente 32h=50 48 254 subb %bl,%al 0x32h 50 48-256=-206 (-206+256) = 0x32h carry 1 Ulteriori esempi divisione : Provate a impostare questi numeri in un breve programmino e provate ad eseguire l'operazione : > DIV %EBX EDX = 0xFFFF:FFFFh MOVL $0xFFFFFFFF , %eax EAX = 0xFFFF:FFFFh MOVL $0xFFFFFFFF , %edx EBX = 0x0000:0100h MOVL $0x00000100 , %ebx Riceverete un errore : Errore in virgola Mobile (SIGFPE aritmetic expresion). Questo accade perche' il risultato e' ( 0x00FF:FFFF ) e' troppo grande per essere contenuto in un registro ! Ulteriori esempi Moltiplicazione : Per quando riguarda MUL, non sorge questo problema. 0xFFFF * 0xFFFF = FFFE:0001 EDX = 0xFFFF:FFFFh MOVL $0xEFFFFFFF EBX = 0xFFFF:FFFFh MOVL $0xFFFFFFFF , %eax , %ebx MULW %ebx RISULTATO EDX:EAX 0xFFFF:FFFE:0000:0001 NOT / NEG Prima di passare oltre; introduco alcune istruzione logiche, al fine di interrompere il discorso di prima e farvi digerire la trattazione matematica a più riprese, così da poter fornire un quadro più ampio, per quanto possa esservi di aiuto; sui vari aspetti della programmazione. In riferimento alle operazione logiche, avevo introdotto NOT, che converte i bit nel suo opposto; cosi tramite una semplice operazione di NOT e sommando 1 bit riuscivo ad ottenere un complemento a 2. numero partenza NOT +1 0101:0101h 1010:1010h 1010:1011h +85 -170 -171 La stessa operazione posso ottenerla tramie NEG numero partenza NEG 0101:0101h 1010:1011h +85 -171 Operandi Esempi reg. gen. NEGL %ebx memoria NEGL mem INCREMENTO / DECREMENTO Simili al contesto C troviamo “dec” e “inc” equivalgono queste due istruzioni all'equivalente “--” e “++” posizionate prima della variabile, cioè valutare prima del termine dell'espressione. INC INC : Questa istruzione ha come scopo di incrementare di 1 unità l'operando di destinazione, utilizzato sovente come contatore nei cicli. sintassi : INC destinazione Operandi Esempi reg. gen. incl %ebx # ++%eax memoria incl mem # ++(*mem) DEC DEC : Questa istruzione ha come scopo di diminuire 1 unità l'operando di destinazione, utilizzato sovente come contatore nei cicli. sintassi : DEC destinazione Operandi Esempi reg. gen. decl %ebx # ++%eax memoria decl mem # ++(*mem) BIT SCAN forward / Reverse x386 dispone di altre istruzioni per la manipalazione dei bit quali BSF/BSR per popeter accedere alla posizione del primo bit a 1 di un operando. Se questi è zero, verrà impostato a 1 il flag ZF (zero flag) e l'operando di destinazione, verrà messo in uno stato non definito. BSF BSF = Bit Scan forward . Cerca nell'operando sorgente il primo bit a 1(partendo da destra) e scrive la sua posizione nell'operando destinazione.Partendo dal bit meno significativo. n.b. La numerazione dei bit parte da 0 ed arriva a 7 in ub byte. sintassi : BSF sorgente,destinazione Operandi Esempi reg. gen. BSFL %eax,%ebx se %eax=0b1010 allora %ebx = 1 memoria BSFW mem,%eax se mem=0b1100 allora %eax = 2 memoria BSCL %eax,mem se %eax=0b1000 allora mem = 3 BSR BSF = Bit Scan reverse . Cerca nell'operando sorgente (partendo a sinistra) il primo bit a 1 e scrive la sua posizione nell'operando destinazione. partendo dal bit più significativo. sintassi : BSF sorgente,destinazione Operandi Esempi reg. gen. BSRL %eax,%ebx se %eax=0b1010 allora %ebx = 3 memoria BSRW mem,%ax se mem=0b1100 allora %eax = 3 memoria BSRL %eax,mem se %eax=0b0001:1000 allora mem = 4 Una libreria di macro per la Libreria C standard Una libreria di macro per le Chiamate di sistema > dalla directory dove contenete i programmi > dalla directory dove contenete i programmi create una directory di nome “include” lettere create una directory di nome “include” lettere minuscole. e salvate il file con il nome di : minuscole. e salvate il fiel con il nome di : stdlibc.a : syscall.a : file : include/stdlibc.a file : include/syscall.a #**************************** # # C Macro Standar C Library # for Gnu Assembler Linux # #**************************** #*************************** # # C_EXIT # #*************************** .macro C_EXIT pushl $0 call exit .endm #**************************** # # C_PUTS # #**************************** # # input : output_string # output: // # #**************************** .macro C_PUTS , __output_string .section .data 0: .asciz "\__output_string" .text pushl $0b call puts addl $4,%esp .endm #*************************** # # Macro System Call # #*************************** .equ .equ SYSCALL SCN_EXIT , 0x80 , 0x01 #*************************** # # SC_EXIT # #*************************** .macro SC_EXIT movl xorl int .endm $SCN_EXIT , %eax %ebx , %ebx $SYSCALL Crescendo di dimensioni i programmi risultano poco gestibili; e' difficile e alquato scomodo lavorare su di un unico file di grande dimensioni, e ogni qualvolta deve essere ricompilato per intero. Una tecnica ormai assodata e' quella di suddividere il programma di grandi dimensioni, in parti più piccoli e più facilemnte gestibili, a pro una volta compilati le subroutine devo solo ricompilare il programma principale o la parte modificata. Al crescere di dimensioni i programmi prendono il nome di “progetti”, via via crescendo e implementandosi con il mondo vengono chiamati “soluzioni”. Nella prassi alcune funzioni vengono chiamate più spesso che altre, anzichè di volta in volta digitare lo stesso codice, posso impacchettarlo in un “nome” e poi digitare quel nome e far riferimento al pezzo di codice in questione, appunto la MACRO. Le macro sono costituite da un “nome” identificativo, da alcuni parametri opzionali cui e' possibile passare ad esse ed da una dicitura che ne termina il significato. esempio : .macro var1,var2 ... .endm .macro C_EXIT ... .endm Quindi digitando il simbolo definito dalla macro il compilatore, provvederà a sostituire il simbolo con il codice contenuto in esso. L'esempio sopra riportato ottiene l'effetto di terminare il programma chiamando la funzione della libreria C. Per comodità utilizzo dei nomi MAIUSCOLI, per le macro, atte ad indentificarle come tali e da altri caratteri al fine di fornirmi più informazioni possibili, prassi comunque tra i programmatori : – – – C_ EXIT MAIUSCOLO riferito alla libreria c ; funzione exit ; Macro Analoga e' la situazione per le macro relative alla chiamate di sistema, SC_EXIT , farò riferimento ad un codice contenuto in una macro che avrà come funzione quella di terminare un programma utilizzando le Chiamate di sistema (SysCall). Ancora in questo caso per maggior chiarezza ho definito un ulteriore simbolo appunto SYSCALL che contiene il numero esadecimale 0x80h che identifica il valore che utilizzerà l'interrupt per le chiamate di sistema. SCN = Sys Call Number ed identifica il numero di riferimento per tale chiamata. .equ la direttiva equ viene utilizzata per assegnare un valore ad un simbolo. La macro successiva e' poco più complessa; se avete notato nei precedenti programmi ogni qualvolta dovevamo visualizzare un risultato mediante la printf facevamo riferimento ad una stringa di formato, nella sezione data. In effetti questa soluzione l'ho trovata piuttosto scomoda, in quanto al crescere delle dimensioni, bisogna continuare a spostarsi col cursore dal punto di utilizzo al punto di origine. Oltretutto dovrò definire tante stringhe di output quante saranno necessarie per visualizzare i risultati, e dovro' devinire tanti simboli quante sono le variabili. Insomma una vera seccatura. In questo caso ci viene incontro GAS tramite l'impostazione delle macro. Macro C_PUTS.La prima parte della macro definisce il Simbolo con qui viene chiamata e dichiara una variabile di ingresso che utilizzerà nel codice definito al suo interno. Notate che la variabile si chiama “__outputstring” e viene utilizzato il valore con “\__outputstring” quindi ogni volta vogliamo utilizzare il valore contenuto in una variabile definita in una macro anteponiamo il simbolo “\”,. Per quanto riguarda i carattere “__” doppio underscore “_” li ho messi nella speranza che in un progretto nessuno chiami un'altra variabile con questo nome. (vedesi variabili globali e locali). A questo punto subentrano due particolarità, ritorna la sezione .data e vengono definite delle variabili locali. Quando gas assemble il codice, dapprima mette il segmento .text, poi quello .data e successivamente gli altri in ordine. Praticamente raggruppa i pezzi qual e la in classi . Il problema che si poneva con la printf e' che dovevamo ogni qual volta definire delle nuove variabili, per indirizzare correttamente la stringa di output. GAS da la possibitlita' di definire dlle variabili locali, e di poterle ripetere con lo stesso nome. GAS considera le variabili locali, quelle che iniziano con un numero ex “1:” che va da (0 a 9) e fa riferimento ad esse con “(Numero)+b minuscolo” ex. “1b”; quindi l'indirizzo della stringa di output a cui puntare sarà $1b. Nell'esempio sopra riportato viene utilizzata la variabile locale 0: . Ancora notiamo l'utilizzo degli argomenti passati alla macro all'interno del codice : “\__output_string” , semplicemente GAS sostituirà la variabile con l'argomento di input. ex C_PUTS “Hello Wolrd!” --> .asciz “Hello World!”, attenzione! l'argomento è Hello Wolrd senza apice “. Ho messo gli apici in quanto troviamo due parole staccate che altrimenti verrebbero considerati come due argomenti. Ora è la volta di una nuova sezione di codice .text, con le rispettive istruzioni che la macro genererà. Se omettessimo l'utilizzo delle variabili locali, e per esempio inserissimo un nostro nome di fantasia alla label:, la seconda volta che utilizzerò la macro, GAS mi informerebbe che il simbolo esiste già. Poi la macro termina con .endm . E' buona norma documentare il codice quanto possibile, per migliorare la manutenzione e la leggibilità (soprattutto verso altri), sicuramente meglio di quanto ho fatto io nell'esempio. La macro SC_EXIT necessita di pochi commenti in quanto la spiegazione e' analoga a prima. Ora vediamo un'applicazione pratica, scrivete questo file : file : nono.s .include "include/stdlibc.a" .include "include/syscall.a" .section .data # .......................................var1 var1: .long 0 # ......................................formato_decimale formato_decimale: .asciz "%d" # ......................................formato_ottale formato_ottale: .asciz "%o" # ......................................carattere carattere: .byte 0 .section .text ######################################### # MAIN # ######################################### .global _start _start: C_PUTS "\nInserisci un numero intero :" pushl $var1 pushl $formato_decimale call scanf addl $8,%esp C_PUTS "\n Inserisci azione menu (0-1) " C_PUTS "\n 0) Conversione in esadecimale : " C_PUTS "\n 1) Conversione in ottale :" #............................return = 2 caratteri CR NL call getchar pushl call addl $carattere getchar $4,%esp # .........................sottrai al carattere ASCII 30 subl $30,%eax # .........................forza 0 o 1 andl $1,%eax # .........................SWITCH call *menu(,%eax,4) C_EXIT # *** # ** #* # *************************************************** MENU 0 conversione_esadecimale: .long . nop .data 1: .asciz "\n [ 0x%0xh ] \n" .text C_PUTS "\n\n *** Visualizza Esadecimale *** : " pushl pushl call addl var1 $1b printf $8,%esp ret # *************************************************** MENU 1 conversione_ottale: .long . nop .data 1: .asciz "\n [ 0%0oo ] \n" .text C_PUTS "\n\n *** Visualizza Ottale *** : " pushl pushl call addl var1 $1b printf $8,%esp ret #............................... # # # #............................... MENU .data menu: .long conversione_esadecimale+4 .long conversione_ottale+4 scelta: .long 0 Commento al file nono.s Nella parte di testa del file : nono.s troviamo l'utilizzo della direttiva .include, che permette di includere moduli di file esterni; e' importante l'ordine in quanto i simboli vengono definiti sequenzialmente, se definisco un simbolo con .equ devo accertami prima di utilizzarlo che GAS,l'abbia compilato. sintassi : .include file Nulla di particolare nella sezione .data seguente in quanto definisco alcune variabili, ed alcune stringhe di formato output. Prima parte.Questo file, descrive il comportamente delle macro sopradescritte, e applica alcuni concetti finora teorici, appresi a riguardo degli indirizzamenti, di alcune istruzioni aritmetiche e logiche e fornisce alcune particolrità a mio avviso interessanti sul compilatore GAS. vedi spegazione. Arrivando alla sezione .text notiamo l'utilizzo di .global anziche .globl, e' la stessa cosa. Dopodichè troviamo l'esempio di utilizzo della prima macro C_PUTS, risulta piuttosto comoda e più pulita come codice rispetto a prima. Segue l'immisione di un numero intero da parte di tastiera mediante la funzione C scanf e la sua memorizzazione nella variabile var1. E fin qui nulla di particolare, ora vediamo la gestione del primo menu, dopo la stampa dell'intestazione del menu seguono apparentemente inspiegabili due funzioni getchar, (libreria c che consente di leggere un tasto dallo standard di input) la seconda fa esattamente il suo dovere, la prima finisce di leggere la sequenza di carattere “\n\r” 10,13, dopo la pressione del tasto RETURN . Memorizza il carattere letto nella variabile “carattere”, che per altro non verrà piu' utilizzata. Da tenere in considerazione, e' il valore di ritrno della funzione getchar, che come molte del C ritornano il valore nel registro generale %eax. Discorrendo sui caratteri asci, pur avendo premuto il tasto '0' o '1' abbiamo come valore di ritorno 0x30h oppure 0x31h, questo e' la posizione nella codifica ASCII , a questo dovrò togliere allora 0x030h per ottenere l'esatto valore. In questo esempio non avendo ancora introdotto le operazioni di confronto e quelle dei sqlti condizionati ho dovuto accertarmi del fatto che effettivamente i numeri introdotti fossero stati '0' oppure '2' , se per sbaglio mi introducevano una lettera il programma andava fuori controllo, come vedremo in seguito continuava l'esecuzione ad una locazione errata (segmentation fault). Quindi a discapito del carattere introdotto ho utilizzato l'istruzione logica AND per azzerare tutt gli altri valori tra quelli del bit 0, cio' quelli he mi interessavano, 0 o 1.Quindi per quanto il risultato introdotto potesse essre errato avevo come conseguenza zero oppure uno! Questo mi e' servito per ottenere correttamente l'indirizzo dei menu in questione. Tramite indirizzameto di base indicizzato, già discusso. In GAS si deve utilizzare '*' che sta ad indicare il contenuto, per quanto riguarda le CALL e JMP indiretti. %eax=0 %eax=1 call call menu(,%eax,4) menu(,%eax,4) *(menu+0*4) *(menu+1*4) La seconda parte della spiegazione e' un poco più complessa.Come prima cosa, bisogna partire dal fondo per capire qualcosa. Non e' possibile utilizzare dei riferimenti FORWARD in gas, in quanto nn sono stati ancora definiti. Corretto, quindi ho impostato questa sezione di .data per ultima, quando avevo finito di scrivere le due routine che mi servivono. La variabile menu: contiene 2 indirizzi diversi che puntano all'inizio della routine, che intendo utilizzare. Notate che dopo tali indirizzi ho aggiunto 4 byte. questo e' per puntare effettivamente all'inizio della sub routine. Mi spiego meglio,se prendiamo l'etichetta conversione_esadecimale: notiamo dopo che è presente un .long e una altro punto . ! Questa etichetta e' trattata come variabile ed il punto '.' sta ad indicare l'esatta posizione dove si trova essa. Quindi ne memorizza la posizione. Possiamo dire in altre parole che l'etichetta conversione_esadecimale: (meglio il simbolo) contiene l'indirizzo di partenza della subroutine. Tuttavia questo indirizzo e' lungo 4 byte, se salto direttamente all'etichetta, la CPU inizierà ad interpretare codice a casaccio, i primi byte dell'indirizzo, tenterà di codificare in qualche modo generando una serie errata di opcode. Quindi aggiungendo il numero fisso di 4, raggiungo il mio scopo e tramite la CALL indiretta punto all'istruzione NOP , o a qualsiasi altra istruzione che segni l'inizio della mia subroutine. Call Non fa nient'altro che prelevare l'indirizzo puntato da menu e un indirizzamento indicizzato, (e' già presente l'aggiunta di 4 byte). L'istruzione NOP , consuma un ciclo e praticamente non fa nulla. Viene usata per alcuni ritardi di ciclo, per allineare sementi, o da alcuni cracker per crackare il codice. Ancora possiamo vedere l'utilizzo delle variabili locali 1:, della macro C_PUTS e di ulteriori sezioni di .text e .data che verranno assemblati insieme come classi successivamente. L'istruzione RET riporta alla condizione prima della call e poi il programma termina con una chiamata alla funzione della libreria C indicata dalla macro. Non so se sono stato troppo chiaro, tuttavia e' il solo modo che conosco per spiegarvelo. Se non capite alcuni concetti, metteteli in pratica ! NOP NOP = No Operand. Nessun operando. Sintassi flag : : NOP nessuno Operando NOP Esempio NOP L'istruzione NOP , consuma un ciclo e praticamente non fa nulla. Viene usata per alcuni ritrdi di ciclo, per allineare segmenti, o da alcuni cracker per crackare il codice. Somma e Sottrazione a 64 BIT l'80x86 era in grado di indirzzare 16 bit, quindi una registro conteneva un massimo di 65°535 valori, poco per quanto riguarda un un reale utilizzo.Successivamente la situazione e' migliorata con l'introduzione dei 386 e dei registri a 32 bit, quindi in grado di indirizzare 4 miliardi di numeri. Tuttavia il problema si pone, come ancora come possibile superare in un'addizione la barriera dei registri ? A questo scopo INTEL ha designato alcune istruzioni apposite che permettono di sommare ai numeri il riporto. Se a 9 sommo 1, scrivo 0 e 1 lo riporto nella colonna a sinistra appena adiacente. 9 1 -----10 + = Cosi per sommare numeri che non sono contenuti nel registro, viene trasportato al numero successivo il CARRY FLAG , o flag di riporto. Vediamo la somma di 2 numeri a 64 bit : EDX:EAX + EBX:ECX = -------------il codice sarà questo, aggiungetelo e create una nuova libreria di macro math.a : .macro add64 addl %ecx , %eax # se grande per essere contenuto genera un riporto adcl %ebx , %edx # potrebbe verificarsi un ulteriore riporto di carry .endm analogamente alla somma ottenniamo la sottrazione a 64 bit con l'ausilio dell'istruzione SBB .macro sub64 sub %ecx , %eax # se piccolo prende in prestito da %edx sbb %ebx , %edx .endm : ADC ADC = Addiziona con CARRY. Se l'operazione precedente ha generato un riporto, utilizza questo per sommarla all'operando di destinazione sintassi : ADC sorgente,destinazione Operandi Esempi reg. gen.,reg gen ADCL %eax,%ebx memoria ADCL mem,%eax SBB SBB = Subtraact with Borrow. Sottrai con prestito. sintassi : SBB sorgente,destinazione Operandi Esempi reg. gen.,reg gen SBBL %eax,%ebx memoria SBBL mem,%eax Addizione / Sottrazione perchè limitarsi ? Precedentemente ho incluso una macro che eseguita la somma o la sottrazione di un numero a 64 bit, tuttavia sorge la necessità di effettuare operazioni sui numeri più grandi. Le macro sotto esposte : – – addxbit subxbit Consentono di sommare numeri di grandi dimensioni, occorre solo indicare la grandezza dei numeri in bit che si vuole sommare o sottrarre. Potete includerla nella vostra libreria personale math.a, tuttavia questa macro a mio avviso prsenta un problema! ne discuterò dopo averla illustrata. Macro add64 : Se prendiamo in considerazione la macro add64 non fa nient'altro che sostituire nel codice i parametri indicati, se non utilizzassimo la macro scriveremo manualmente le due righe di codice, se nella mio programma necessito di utilizzare più volte la stessa macro, oppure di scriverla a mano, la situazione non cambia poi di molto le dimensioni del codice rimangono le stesse; Le cose cambiano se utilizzo più volte la 2^nda macro addxbit immaginate di richiamare 10 volte nel codice la stessa, le dimensioni del codice aumenteranno notevolmete. e' un po' come fare un paragone con le funzioni INLINE del c vanno bene se non gonfiano troppo. Per ovviare questo inconveniente la stessa macro verrà scritta come funzione, mi basterà accedervi con una call dopo aver passato i parametri necessari. SUBxBIT ADDxBIT .macro addxbit, nbit, source, dest .macro subxbit, nbit, source, dest pushl %eax pushl %eax pushl %ebx pushl %ebx pushl %ecx pushl %ecx pushf pushf xorl %ebx,%ebx xorl %ebx,%ebx movl $\nbit , %ecx movl $\nbit , %ecx shrl $5 shrl $5 , %ecx , %ecx # dividi * 32 ottieni numero ripetizioni # dividi * 32 ottieni numero ripetizioni movl movl %ecx , %ebx %ecx , %ebx # ottieni riferimento a low word # ottieni riferimento a low word shll shll $1 , %ebx $1 , %ebx # moltiplica %ebx*2 # moltiplica %ebx*2 adcl subl %ecx , %ebx %ecx , %ebx # risultato %ebx*3 # risultato %ebx*3 clc clc 0: 0: movl \source(%ebx) , %eax movl \source(%ebx) , %eax adcl %eax , \dest(%ebx) adcl %eax , \dest(%ebx) subl $4 , %ebx subl $4 , %ebx loopl 0b loopl 0b popf popf popl %ecx popl %ecx popl %ebx popl %ebx popl %eax popl %eax .endm file : undici.s .endm .include "include/dffcld.a" .include "include/stdlibc.a" .include "include/syscall.a" .include "include/math.a" #################################### .data sorg: .long .long .long .long 0x11223344 #128bit 0xaabbccdd 0x55667788 0xff0099ee dest: .long .long .long .long 0x01 0x01 0x01 0x01 #128 output_string: .asciz " \n [ %08x:%08x:%08x:%08x ] \n " .globl .text _start _start: addxbit 128 sorg dest pushl pushl pushl pushl pushl call addl dest+12 dest+8 dest+4 dest $output_string printf $20,%esp subxbit 128 sorg dest pushl pushl pushl pushl pushl call addl C_EXIT dest+12 dest+8 dest+4 dest $output_string printf $20,%esp Commento : Il file undici.s si appoggia alle macro addxbit e subxbit non ha particolare interesse. Entrambe le macro operano per così dire in modo simile add eccezione di una istruzione. Viene fatto un salvataggio dei parametri nello stack per preservarli così dato che viene modificato il carry flag viene fatta una copia. Il registro ECX indica di quante word e' formato il numero, ma prima deve essere diviso per 32 (shrl $5,%ecx ), in quanto per comodità dell'utente l'inserimento dei valori viene fatto in bit. ECX è legato a loop e viene decrementato ad ogni passaggio all'istruzione LOOP quando e' zero il ciclo termina. (LOOP viene discusso più approfonditamente nel capitolo : tutto sui flag!). Viene utilizzato l'indirizzamendo di base indiretto, così occorre indirizzare la somma dal bit meno significativo in poi. EBX di partenza contiene l'offset di partenza del numero sorgente e destinatario in questione, viene decrementato di 4 (di una word alla volta), quindi se abbiamo 128 bit allora ECX sarà 4 cioè 4 word allora il punto di partenza di sorgente sarà sorg+ebx (sorg+12) appunto ebx*3. Una traslazione verso sinistra ne raddoppia il significato. CLC viene utilizzato per azzerare un eventuale carry di riporto che falserebbe il primo risultato se settato diversamente. Così di procede al al ciclo per sommare le varie word, sfruttando la proprietà cumulativo dell'addizione, cioè il risultato della somma la metto direttamente nella memoria. Generalmente quando si è alle prime armi con l'assembly la cosa più ovvia sarebbe quella di salvare il risultato intermedio in un registro e poi questo copiarlo nella memoria. Così e' più rapido. Il ciclo continua finchè non vengono passate in rassegna tute le word decrementando di $4 ebx. La routine può essere migliorata, eventualmente tenendo conto di un ultimo carry di riporto (JC) e segnalare così un errove di Overflow. Breve spiegazione di loop. L'istruzione LOOP consente di ripetere l'esecuzione del programma ad un'etichetta indicata, per un numero di volte specificato dal registro %ecx, il quale verrà decrementato ogni volta al passaggio di tale istruzione. Termina quando %ecx = 0 ; CAPITOLO 6 Tutto sui Flag ! (o quasi) Tutto sui Flag ! (o quasi) Ho suddiviso l'esposizioni in piu' capitoli relativi all'aritmetica ed alla logica, ora per spezzare la routine, inserisco questo capitolino a cavallo di un altro di aritmetica, per introdurre una trattazione, possibilmente estesa sui Flag, sui vari loro settaggi e sulle istruzioni che ne condizionano i salti; Riepilogo FLAG BIT Flag 0 2 Tipo Descrizione CF Carry Questo indica che un riporto negtivo o positivo borrow e' stato generato dal bit piu' significativo PF Parity Quando vale 1 l'ultima istruzione ha generato un numero pari di bit ZF Zero Quando vale 1 indica che l'ultima istruzione ha generato un risultato di zero SF Sign Quando vale 1 indica che nell'utlima operazione il bit piu' significativo e' 1 IF Interrupt Se 1 le interruzioni esterne di tipo INTR sono abilitate (settato da CLI e STI) DF Direction controlla la direzione di una stringa di dati se DF=1 allora SI-– e DI -se DF =0 allora SI++ e DI ++ Si possono settare con queste istruzioni CLD e STD OF Overflow Quando 1 indica che durante l'utlima operazione si e' avuto un overflow 5 6 7 9 10 11 Questa e' una tabella riassuntiva, di quella precedentemente esposta la capitolo 1; In linea di massima, ad ogni flag e' associato un'istruzione di : – – – settaggio ; azzeramento ; salto condizionato . FLAG SET CLEAR JMP FLAG=0 JMP FLAG=1 Carry (CF) STC / (CMC) CLC / (CMC) JNC JC Zero (ZF) // // JNZ JZ Sign (SF) // // JNS JS Parity (PF) // // JNP / JPO JP / JPE Direction (DF) STD CLD // // Interrupt (IF) STI CLI // // CLC / STC / CMC CLC = Clear Carry Flag STC = Set Carry Flag CMC = Complement carry Flag formato formato format : : : : azzera i flag carry. : imposta a uno il carry : Complementa il carry flag CLC STC CMC Operandi Esempi istruzione CLC # CF=0 istruzione STC # CF=1 istruzione CMC # se CF=1 allora CF=0 JC / JNC JC = jump carry. Esegui l'istruzione di salto se il carry eè impostato a 1 JNC = jump not carry. Esegui l'istruzione di salto se il carry e' zero Sintassi Sintassi : : JC JNC destinazione destinazione Operandi Esempi istruzione memoria JC mem # se CF=1 istruzione memoria jnc mem # se CF=0 Esempio : Verifica che il meno significativo sia '1' bt jc ... $0,%eax bit_zero_impostato JP / JNP JPE / JPO Jp = jump parity. Esegui l'istruzione di salto se il PF = 1 JNp = jump not parity. Esegui l'istruzione di salto se il pF = 0 Sintassi Sintassi : : Jo JNo destinazione destinazione Operandi istruzione memoria istruzione memoria Esempi Jp mem # se pf = 1 jpe mem # se pf = 1 jnp mem # se pf = 0 jpo mem # se pf = 0 Esempio : movb $0b00001111 , %al jp numero_pari_di_bit_settati_a_uno movb $0b00001111 , %al jp numero_pari_di_bit_settati_a_uno mobb $0b00011111 , %al jpe numero_dispari_di_bit_settati_a_uno mobb $0b00011111 , %al jpo numero_dispari_di_bit_settati_a_uno JZ / JNZ JZ = jump zero. Esegui l'istruzione di salto se il ZF = 1 JNZ = jump not zero. Esegui l'istruzione di salto se il ZF = 0 Sintassi Sintassi : : JZ JNZ destinazione destinazione Operandi Esempi istruzione memoria JZ mem # se ZF = 0 istruzione memoria jnz mem # se ZF = 1 Esempio : Verifica che il meno significativo sia '1' sub jz %eax,%ebx se_risultato_zero JS / JNS JS = jump sign. Esegui l'istruzione di salto se il SF = 1 JNS = jump not sign. Esegui l'istruzione di salto se il SF = 0 Sintassi Sintassi : : JS JNs destinazione destinazione Operandi Esempi istruzione memoria JS mem # se sf = 1 istruzione memoria jns mem # se sf = 0 Esempio : movb $0b10000000 , %al js numero_negativo JO / JNO Jo = jump overflow. Esegui l'istruzione di salto se il OF = 1 JNo = jump not overflow. Esegui l'istruzione di salto se il OF = 0 Sintassi Sintassi : : JO JNO destinazione destinazione Operandi Esempi istruzione memoria Jo mem # se of = 1 istruzione memoria jno mem # se of = 0 Esempio : movb $128 , %al addb $128 , %al jo superamento_capatica_registro CLD / STD CLD = Clear Directional Flag : azzera il flag di direzione STD = Set Directional Flag : imposta il flag di direzione formato formato : : CLD STD Operandi Esempi istruzione CLD # DF=0 istruzione STD # DF=1 se DF = 0 --> qualsiasi istruzione che fa riferimento alle “stringhe” , incrementa (++)i registri ESI EDI se DF = 1 --> qualsiasi istruzione che fa riferimento alle “stringhe” , decrementa (--) i registri ESI EDI CLI / STI CLI = Clear interrput Flag STI = Set interrupt Flag : azzera il flag di interrupt : imposta il flag di interrupt formato formato CLI STI : : Operandi Esempi istruzione CLI # iF=0 abilita l'interruzione istruzione STI # if=1 disabilita l'interruzione SETcc Un'altra serie di istruzioni per la manipolazione dei flag la troviamo nella istruzione SET con le rispettive varianti. Questa istruzione memorizza UNO o ZERO, nell'operando DESTINAZIONE, se la specifica condizione è VERA o FALSA. Sintassi : SETcc Destinazione Operandi Esempi istruzione SETcc mem istruzione SETcc %al ex: # byte di memoria # registro di un byte movb $0x11110000 , %al test $0x00000010 , %al sete risultato desc: se il bit 1 di al è uguale a 1 allora metti 1 all'indirizzo di memoria : risulato. ex: movb $128 , %al movb %al , %bl addb %bl , %al setc %dl desc: se si e' verificato un riporto carri metti a 1 il registro dl cc puo' assumere le seguenti lettere, anche se devono essre disposte in modo valido. A B C E G L N O P S Z ABOVE sta per sopra BELOW Sta per sotto Carry Equal sta per pari o uguale Greatersta per maggiore Less sta per minore No appunto nega la condizione precedente Overflow superamento di capacità Parità salto con bit a 1 pari Segno Zero SETcc unsigned CF ZF SETA / SETNBE set above a> b 0 0 SETAE / SETNB set above o equal a>=b 0 * SETB / SETNAE set below a< b 1 * SETBE / SETNA set below o equal a<=b 1 1 unsigned CF ZF 0 0 SETcc SETNA / SETBE set not above a> b SETNAE / SETB set not above equal a>=b SETNB / SETAE set not below a< b SETNBE / SETA set not below equal a<=b SETcc * 0 1 * 1 1 signed CF ZF SETG / SETNLE set greater a> b OF 0 SETGE / SETNL set greater o equal a>=b OF * SETL / SETNGE set less a< b !OF * SETLE / SETNG set less o equal a<=b !OF 1 SETcc signed CF ZF SETNG / SETNLE set not greater a> b OF 0 SETNGE / SETL set not great equal a>=b OF * SETNL / SETGE set not less a< b !OF * SETNLE / SETG set not less equal a<=b !OF 1 SET!cc flag cc !cc SETcc SETC SETNC carry set 1 se cf = 1 set 0 se cf = 0 SETE SETNE zero set 1 se zf = 1 set 0 se zf = 0 SETO / SETPO SETNO overflow set 1 se of = 1 set 0 se of = 0 SETP SETPE SETNP parity set 1 se pf = 1 set 0 se pf = 0 SETS SETNS sign set 1 se sf = 1 set 0 se sf = 0 SETZ SETNZ zero set 1 se zf = 1 set 0 se zf = 0 PUSHF/POPF (16bit) Queste istruzioni manipolano i flag per inserirli nello stack. La prima mette nello stack i 16 bit inferiori del regitro EFLAGS. PUSHF = Carica i Flag (16bit) nello stack : %esp -= 2 POPF = Estrai i flag (16bit) dallo stack : %esp += 2 Operandi Esempi istruzione pushf istruzione popf PUSHFD/POPFD (32bit) Queste istruzioni manipolano i flag per inserirli nello stack. La prima mette nello stack i 32 bit inferiori del regitro EFLAGS. PUSHFD POPFD = Carica i Flag (32bit) nello stack : %esp -= 4 = Estrai i flag (32bit) dallo stack : %esp += 4 Operandi Esempi istruzione pushfd istruzione popfd PUSHA/POPA (16bit) Inserisci/estrai nello stack tutti i registri generali a 16 bit. Registri : AX CX DX BX SP BP SI DI Operandi Esempi istruzione pusha istruzione popa PUSHAD/POPAD (32bit) Inserisci/estrai nello stack tutti i registri genberali a 32 bit. Registri : EAX ECX EDX EBX ESP EBP ESI EDI Operandi Esempi istruzione pushad istruzione popad SAHF / LAHF SAHF = questa istruzione memorizza nel registro eflag i bit contenuti in ah ed esegue un AND logico 0b11010101 LAHF = Load Flag in ah; carica NEL registro ah i bit del registro eflag ed esegue un AND logico 0xffh Operandi Esempi istruzione SAHF # AH=Eflags(8bit) , poi AND $0b11010101 istruzione LAHF # ah = eflag(8bit) , pot AND $0xFF Esempio : LAHF shrb andb $4%ah $1,ah ora ah contiene 1 zero a seconda se il flag ausiliario e' settato. Salti Condizionati Le istruzioni di salto condizionato assumo la forma generica di “Jxx” dove “xx” sono dei suffisi validi per identificare la condizione di salto. Da tenere in cosiderazione che tutti i salti di tipo Condizionato, non possono indirizzare codice al di là di -128 / +127 byte di distanza dall'istruzione stessa. (Eccezzioni) SHORT : il salto è limitato nel range da -128 a +127 NEAR : il salto è limitato nel range da -32768 a +32767 FAR : il salto può effettuarsi anche tra diversi segmenti di codice. I salti NEAR accettano valori compresi tra -32768 / +32767 (per default dagli x386 ) xx puo' assumere le seguenti lettere, anche se devono essre disposte in modo valido. A B C E G L N O P S Z ABOVE BELOW Carry Equal Greater Less No Overflow Parità Segno Zero sta per sopra Sta per sotto sta per pari o uguale sta per maggiore sta per minore appunto nega la condizione precedente superamento di capacità salto con bit a 1 pari I riferimenti in testo normale sono gia stati discussi, precedentemente, ora visitiamo i riferimenti con il testo in grassetto. Dicevo che in numeri in linguaggio macchina, sono sempre unsigned, in effetti e' vero anche se poi nella pratica riusciamo ad ottenere i numeri positivi e negativi, ma per un calcolatore questo e' indifferente; memorizza nella memoria solo un numero. Il significato che assume e' direttamente legato alle istruzioni e condizioni che appunto ne determinato il modo operativo. Quindi per poter distinguere i numeri Positivi da Negativi, il processore si avvale di alcuni salti condizionati : unsigned 0 / 255 CF ZF signed -128 / 127 SF ZF a> b JA JNBE 0 0 a> b JG JNLE OF 0 a >= b JAE JNB 0 * a >= b JGE JNL OF * a< b JB JNAE 1 * a< b JL JNGE !OF 0 a <= b JBE JNA 1 1 a <= b JLE JNG !OF * A fianco vengono indicate le condizioni di apparteneza a UNSIGNED o SIGNED. In effetti un numero negativo genera quasi sempre un overflow. JCXZ / JECX JCXZ : fa riferimento al registro CX e esegue l'istruzione se questo e' zero (16 bit) JECX : fa riferimento al registro ECX e esegue l'istruzione se questo e' zero (32 bit) Operandi Esempi istruzione JCXZ indirizzo istruzione JECX indirizzo JE / JNE JE = Salta se uguale ZF = 1 JNE = Salta se diverso ZF = 0 Operandi Esempi istruzione JE indirizzo istruzione JNE indirizzo SALTI INCONDIZIONALI JMP JMP : salto Incondizionato FAR NEAR Operandi Esempi istruzione jmp address istruzione jmp *(%eax) LOOP LOOP : ciclo . Tra i salti condizionali certamente il più utilizzato è LOOP. Questa istruzione si avvale del registro CX (word) (loopw) o del registro ECX (long) (loopl), e decrementa il registro CX/ECX ad ogni ciclo verso l'indirizzo di riferimento. formato : loop indirizzo Istruzione : LOOP : decrementa CX / ECX e continua verso l'indirizzo finchè ECX / CX > 0 LOOPE / LOOPZ : decrementa CX / ECX e continua verso l'indirizzo finchè ECX / CX >= 0 LOONE / LOOPNZ : decrementa CX / ECX e continua verso l'indirizzo finchè ECX / CX != 0 LOOPW : decrementa CX e continua verso l'indirizzo finchè CX > 0 LOOPEW/ LOOPZ LOONEW/ LOOPNZ : decrementa CX e continua verso l'indirizzo finchè CX >= 0 : decrementa CX e continua verso l'indirizzo finchè CX != 0 LOOPL LOOPEL/ LOOPZ LOONEL/ LOOPNZ : decrementa ECX e continua verso l'indirizzo finchè ECX > 0 : decrementa ECX e continua verso l'indirizzo finchè ECX >= 0 : decrementa ECX e continua verso l'indirizzo finchè ECX != 0 MOV Condizionali Ebbene si pensavo che erano finiti e invece ecco che mamma x86 ha sfornato un'altra serie di istruzioni condizionali, devo dire personalmente che le trovo carine. si tratta delle istruzioni CMOVcc appunti sposta il dato dall'operando sorgente a quello destinatario se la condizione risulta vera. Queste istruzioni funzionano solamente con registre a 16 o 32 bit. Esempio : movl $10 ,%eax movl $9 ,%ebx cmoval %eax ,%ebx ora %ebx contiene 9 condizione : se (unsigned) %eax > (unsigned) %ebx allora %ebx = %eax vediamo ora in rassegna tutte le istruzioni di MOV condizionale : unsigned 0 / 255 CMOVA CMOVNB E A> B CMOVAE CMOVNB A >= B CMOVB CMOVNA E A< B CMOVBE CMOVNA A <= B CMOVcc CF ZF 0 0 0 1 1 signed -128 / 127 SF ZF CMOVG CMOVNLE A> B OF * CMOVGE CMOVNL A >= B OF * * CMOVL CMOVNGE A< B !OF * 1 CMOVLE CMOVNE A <= B !OF 1 CMOV!cc flag cc !cc CMOVC CMOVNC carry mov se cf = 1 mov se cf = 0 CMOVE CMOVNE zero mov se zf = 1 mov se zf = 0 CMOVO CMOVNO overflow mov se of = 1 mov se of = 0 CMOVP / CMOVPE CMOVNP / CMOVPO parity mov se pf = 1 mov se pf = 0 CMOVS CMOVNS sign mov se sf = 1 mov se sf = 0 CMOVZ CMOVNZ zero mov se zf = 1 mov se zf = 0 0 Timer Software & Accenno Floating Point Inserite questo listato : dieci.s e poi apportate le relative modifiche alla libreria di macro. le routine del timer sofware potete mettere in dffcld.a o dove volete. file : dieci.s .include "include/dffcld.a" .include "include/stdlibc.a" .include "include/syscall.a" #*************************************** # # rdtsc_start # #************************************** .equ rdtsc_start , timer_start .macro timer_start rdtsc pushl %eax pushl %edx .endm #************************************** # # rdtsc_stop # #************************************* .equ rdtsc_stop , timer_stop .macro timer_stop rdtsc popl %ebx popl %ecx subl %ecx,%eax sbbl %ebx,%edx .endm #************************************ # # rdtsc_print # #*********************************** .equ rdtsc_print , timer_print .macro timer_print .data 0: .asciz "\nTimer : 0x%08x:%08x \n" .text pushl %eax pushl %edx pushl $0b call printf addl $12,%esp .endm #################################### .data reale: .double 9.1 formato_double: .asciz " %g " .globl _start _start: .text timer_start timer_print fldl fsqrt fstpl reale pushl pushl pushl call addl reale+4 reale $formato_double printf $12,%esp reale timer_stop timer_print C_EXIT commento : Il file essenzialmente si suddivide in 2 parti. Una prima e' una serie di macro, per impostare l'utilizzo dell'istruzione RTDSC, con le tre comode funzoini di richiamo timer_start, timer_stop, timer_print. La seconda parte illustra l'utilizzo dei numeri floating point e in particolare. Come potete vedere per caricare correttamente i valori utilizzo il suffiso dopo le istruzioni “l” long, in quanto se caricavo da un float anzichè da un double come in questo caso dovevo mettere il suffiso “s” cioè single. flds fldl (floating Point Load Single) (floating Point Load Double) Dopodichè il numero viene caricato nllo stack dei floating point che va da ST0 a ST7, questo stack non prende in considerazione la tecnica LIFO o FIFO, ma e' possibile acedervi direttamente ai numeri memorizzati riferendosi con un numero ST, ST(0) , ST(7). Una volta calcolata la radice quadrata, rimemorizzeò il numero, in memoria, togliendolo dallo stack ST0 fstpl Store Floating Point POP double p.s. Quando carico un registro con flds il registro in cima allo pseudo-stack da st0 passa a st1 creando spazio per contenere il nuovo registro. Il Timer Software Queste routine, vi consentono di misurare le prestazioni delle vostre routine. Non è di certo un metodo efficacissimo a della di molti hacker vedi comp.lang.os.x86 dove scrivono i massimi esperti di informatica, tuttavia e' una soluzione molto economica se paragonata a dispositivi hardware. Il pentium e ora anche gli Atlhlon sono dotati all'interno di un proprio timer. E' possible leggerlo tramite l'istruzione RDTSC ( ReaD Time Stamp Counter ). Se non disponete di tale istruzione nell'assembleatore da voi usato. Inserite in sequenza nel segmento codice quest istruzioni : .byte 0x0fh .byte 0x31h Queste codici operativi identificano la suddetta istuzione. L'istruzione ritorna il valore del contatore interno in due registri di peso basso e alto EDX:EAX . Cosicché tramite una successiva operazione di sottrazione possiamo calcolare il tempo impiegato. In seguito descriverò i registri di controllo e istruzioni privilegiate, tuttavia se leggo da uno di questi non dovrei avere problemi. Tuttavia per controllare se questa istruzione nel vostro S.O. e' privilegiata, basta impostare queste semplici righe di codice mov test CR4 , %eax # carica %eax con il registro di controllo 4 $4 , $eax # verifica il contenuto del bit 5 jz non_privilegiata CAPITOLO 7 Aritmetica & Logica 3 Aritmetica & Logica 3 Partiamo subito con 2 istruzioni : IDIV IDIV = Integer Sign Division. Questa istruzione e' analoga a DIV ma tiene in considerazione se il numero è negativo / positivo. Il numero prende in considerazione la coppia dei regitri edx:eax formato : IDIV sorgente disisore implicito divisore quoziente resto ax dx:ax edx:eax byte word dword al ax eax ah dx edx Operandi Esempi registro idivl %ebx # edx:eax / ebx = eax resto edx registro idivw %bx # dx:ax / bx = ax resto dx registro idivb %cl # ax immediato divl $2 # ERRATO / cl = al resto ah IMUL imul = Integer Sign Multiply. Questa istruzione moltiplica 2 numeri con segno. moltiplicatore implcito moltiplicatore risultato al ax eax byte word dword ax dx:ax edx:eax Operandi Esempi registro imull %ebx # eax * ebx = edx:eax registro imulw %bx # ax * bx = dx:ax registro imulb %cl # al * cl = ax immediato Imul $2 # ERRATO Un problema di conversione !? subito risolto ! Il microprocessore tratta i dati tramite i registri, questi sono in grado di indirizzare, byte word e dword. (al,ax,eax). A volte sorge la necessità di convertire i dati contenuti in un registro (al) in uno di maggiori capacità (eax). Ecco venirci incontro le istruzioni di conversione. CBW / CWD / CDQ / CWDE CBW / CWD / CDQ / CWDE Convert Convert Convert Convert from from from from esempio Byte to Word Word to Dword CWDw Dword to Qword Word to DWord Extended CBWb %al %ax CDQl %eax CWDE %ax risultato/registri DX:AX AX EDX:EAX EAX Da notare che queste istruzioni mantengono inalterato il BIT del segno. Operandi Esempi registro cbwb %al registro cwdw %ax registro cdql %eax registro cwde %ax MOVSX / MOVZX MOVSX = move with sign extension. Questa istruzione copia un operando in un operando destinazione formato : movsx sorgente,destinazione Operandi Esempi registro movsxl %ax , %eax memoria movsxw %edi , (%esi) registro movsxb %cl , %dx MOVZX = move with zero extension. Questa istruzione copia un operando in un operando destinazione ed estende di restanti zeri l'operando detinazione. formato : movzx sorgente,destinazione Operandi Esempi registro movzxl %ax , %eax memoria movzxw %edi , (%esi) registro movszb %cl , %dx Scorrimento a 64 bit SHLD SHLD = Shift Left double formato : shld sorgente , destinazione , count Questa istruzione esegue lo scorrimento a 64 bit tra due operando a 32 bit, di tanti posti quanto indicato da count, che viene mascherato a 0x1fh Operandi Esempi reg. , reg. , imm. shld %edi , mem , 7 reg. , reg. , imm shld %edi , %esi , 7 reg. , reg. , %cl shld %edi , mem , %cl reg. , reg. , %cl shld %edi , %esi , %cl SHRD SHRD = Shift Right double formato : shrd sorgente , destinazione , count Questa istruzione esegue lo scorrimento a 64 bit tra due operando a 32 bit, di tanti posti quanto indicato da count, che viene mascherato a 0x1fh Operandi Esempi reg. , reg. , imm. shrd %edi , mem , 7 reg. , reg. , imm shrd %edi , %esi , 7 reg. , reg. , %cl shrd %edi , mem , %cl reg. , reg. , %cl shrd %edi , %esi , %cl SWAP! XCHG XCHG = Exchange Formato : XCHG destinazione , destinazione Questa istruzione scambia tra loro, (swap) due operandi tra loro o con un indirzzo in memoria. Operandi Esempi reg. , reg. xchgl %eax , %ebx reg. , mem. xchgw %ax , mem bswap bswap : scambio byte in un registro a 32 bit sintassi : bswap destinazione , destinazione Operandi Esempi reg. , reg. bswapl %eax reg. , reg. bswapw %ax errore solo 32 bit xadd xadd : scambio byte in un registro e somma sintassi : xadd sorgente , destinazione al termina dell'operazione : sorgente = destinazione ; destinazione = destinazione + sorgente ; Operandi Esempi reg. , reg. xaddl %eax , %ebx reg. , mem. xaddw %ax , mem Qualche istruzione di aggiustamento aaa / aad / aam / aas / daa / das Queste istruzioni lavorano sui BCD, sui numeri Binari Decimali Compatti.Utilizza i registri AL e AH a seconda del tipo di operazione che vogliamo ottenere. sintassi : istruzione Operandi Esempi reg. , reg. aaa AAA Ascii Adjust after Action Questa istruzione prende il numero contenuto nel registro al e lo converte in BCD nei registri ah:al partenza ah al 05 07 add ah,al risultato ah al 00 0c 05+07 aaa risultato ah al 01 02 AAD Ascii Adjust Before Division Questa istruzione prende un numero BCD in ah:al e lo mette in al in formato esadeciamale numero 39d partenza ah al 03 09 aad risultato ah al 00 27h numero 21d partenza ah al 02 01 aad risultato ah al 00 15h AAM Ascii Adjust After Multiplication Prende il numero Bcd (compatto) contenuto in al e lo converte in ah:al ah al 00 45 aam ah al 04 05 AAS Ascii Adjust after Subtraction L'istruzione assicura la corretta esecuzione di una sottrazione. Se il valore al genera un prestito la cifra in al viene forzata alla dimensione corretta e viene decrementato ah al=-1 ah al 00 fe aas ah al ff 08 DAA Decimal Adjust al After Addition Dopo aver eseguito un'operazione, con numeri esadeciamle il risultao viene convertito in BCD compatto numero partenza daa risultato 8bh ah al 00 8b ah al 00 91 ffh 00 ff 00 65 DAS Decimal Adjust AL after subtraction Simile alla precedente, dato che la sottrazione viene eseguita in esadecimale, il risultsato viene poi convertito in BCD compatto. numero partenza 2fh ah al 00 2f ffh 00 ff das risultato 00 ah 29 al 00 99 Convenzioni di Chiamata Inserite questo file : dodici.s .include .include .include .include "include/dffcld.a" "include/stdlibc.a" "include/syscall.a" "include/math.a" #################################### .data sorg: .long 0x11223344 #128bit .long 0xaabbccdd .long 0x55667788 .long 0xff0099ee dest: .long .long .long .long 0x01 0x01 0x01 0x01 #128 output_string: .asciz " \n [ %08x:%08x:%08x:%08x ] \n " _start: .globl _start .text pushl pushl pushl call addl pushl pushl pushl pushl pushl pushl call addl $dest $sorg $128 _addxbit $12,%esp %eax # salva il valore di ritorno dest+12 dest+8 dest+4 dest $output_string printf $20,%esp popl %ebx SC_EXIT # ottieni il valore di ritorno # linux sys call EXIT (%ebx) ############################### # ############################### .globl _addxbit .type _addxbit, @function _addxbit: 0: pushl %ebp movl %esp , %ebp # salva ebp corrente # utilizza ebp come stack pointer xorl movl shrl %edi 8(%ebp) $5 , %edi , %ecx # 1° parametro : nbit , %ecx movl shll addl %ecx $1 %ecx , %edi , %edi , %edi clc movl movl 12(%ebp) , (%ebx,%edi,1) , %ebx %eax movl adcl 16(%ebp) %eax , , %ebx (%ebx,%edi,1) # 3° parametro : dest (&dest+%edi) subl $4 , %edi loopl 0b movl popl %ebp %ebp ret , %esp # 2° parametro : sorg (&sorg) Commento : Questo file produce gli stessi risultati della macro esposta nel capitolo precedente per quanto rigiarda la somma di grandi numeri. Purtroppo se chiamiamo più volte la macro all'interno del programma vedremo il codice gonfiarsi, con l'utilizzo delle funzioni, questo non accade in quanto devo solo passare i parametri richiesti, anche se il funzionamento e' più complesso. La prima parte del programma si occupa di inizializzare nel segmento .data due numeri a 128 bit : sorg. e dest. E tramite un formato di stampa di visualizzarne il risultato. La funzione _addxbit necessita di tre parametri in ingresso : – – – dest : un indirizzo di destinazione dove mettere il risultato sorg : un indirizzo dove è contenuto il numero che andrà sommato a dest bit : il numero di bit dei due valori da sommare Prima della chiamata alla funzione questi andranno spinti nello stack, e questo diminuisce il suo valore; Potete vedere che il valori vanno messi in ordine inverso, questo dipende molto dal linguaggio con cui vi interfacciate, nel pascal accade il contrario. Verificate nel manuale del vostro compilatore come gestisce il passaggio di parametri alle funzioni. In questo caso utilizzo la convenzione di chiamate del C. Al ritorno dalla funzione dovrò ripristinare la posizione iniziale dello stack, sommando al registro %ESP tanti byte quanti inseriti dalle operazioni di push. Nel nostro caso avendo inserito 3 long, occorrerà addizionare 12 byte al registro %esp. Quindi il programma termina in modo usuale. Più complicata e' la parte della subroutine, nella quale si occupa di gestire i valori immessi. Inanzitutto vidiamo una dichiarazione .globl _addxbit che dice di rendere pubblica questa funzione, ed una ulteriore definizione .type che indica appunto che si tratta di una funzione. (.type _addxbit, @function). Effettivamente questo passaggio non sarebbe stato necessario, serve quando voglio dialogare con il compilatore C, viene messo solo a scopo di completezza. A questo punto nello stack avre i 3 valori long e l'indirizzo di ritorno 12(%esp) $dest 8(%esp) $sorg 4(%esp) $128 (%esp) $return address # # 2° # 3° parametro parametro 1° parametro I valori che precedono il regitri ESP son i valori che andranno sommati per ottenere la corretta posizione dei parametri. Tuttavia nelle convenzioni di chiamata viene salvato ulteriormente il registro EBP nello stack e a questo viene associato il registro ESP. a Questo pounto la situazione all'inizio della funzione dopo le due operazioni iniziali è questa : 16(%esp) $dest 12(%esp) $sorg 8(%esp) $128 4(%esp) $return address (%esp) %ebp # # # 3° 2° 1° parametro parametro parametro A meno che non inserica delle variabili locali alla funzione e presto vedremo come fare. Le viariabili passate alla funzione iniziano da 8(%esp), procedendo a ritroso. In fine vengo ripristinati i valori dallo stack (%ebp e %esp) Variabili Locali alle funzioni ############################### # ############################### .globl _subxbit .type _subxbit, @function _subbit: pushl %ebp movl %esp , %ebp # salva ebp corrente # utilizza ebp come stack pointer #................................................... Variabili Locali subl movl $4, %esp $1, -4(%ebp) # fai spazioni per 4 byte # inizializza la variabile locale con 1 #.................................................................... 0: xorl movl shrl %edi 8(%ebp) $5 , %edi , %ecx # 1° parametro : nbit , %ecx movl shll addl %ecx $1 %ecx , %edi , %edi , %edi clc movl movl 12(%ebp) , (%ebx,%edi,1) , %ebx %eax # 2° parametro : sorg (&sorg) movl sbbl 16(%ebp) %eax , , %ebx (%ebx,%edi,1) # 3° parametro : dest (&dest+%edi) subl $4 , %edi loopl 0b #........................................ in %eax il valore di ritorno movl -4(%ebp), %eax movl popl ret %ebp %ebp , %esp Commento : Questa subroutine e' uguale alla precedente, anzichè la somma viene presa in considerazione la sottrazione. In più viene trattata una variabile locale ed un rispettivo codice di ritorno. Come potete vedere, il registro %ebp rimane inalterato, e viene sottratto per fare spazio, $4 byte dal resgistro %esp , questo perchè se abbiamo altra chiamate a funzioni ricorsive o non, il registro %ebp viene di volta in volta salvato, e non viene influenzato con i vari indirizzi di ritorno o altre manipolazioni dello stack come %esp. Quindi ci riferiamo dopo aver creato spazio alle variabili locali con -4 appunto aggiungendo elementi allo stack diminuisco la catasta. Importante per rispettare lo standard e' il valore di ritorno, %eax che generalmente contiene un codice. (vedi prototipi funzioni che intendi utilizzati per ulteriori chiarimenti). Ancora qualche dettaglio Notiamo che i parametri non sono prelevati dallo stack ma vi si accede direttamente dalla subroutine,uno dei motivi e' che l'ultimo parametro è l'indirizzo di ritorno, quindi diventa scomodo prelevarlo, per poi rimetterlo nello stack. Ancora se noi mettiamo il parametro in un registro, questo nel corso della subroutine può essere modificato, anche se utilizzando alcuni parametri del GCC possiamo forzare il compilatore ad utilizzare i registri. Ancora per convenzione di chiamata al primo parametro si accede con 8(%ebp) e le variabili locali come abbiamo visto -4(%ebp) questo permette di non modificare nessun riferimento ai parametri passati, se utilizzassimo delle variabili locali e allocheremo altro spazio allora di volta in volta dovremo cambiare i riferimenti al primo o agli altri parametri. Nelle convenzioni di chiamata il C, salva il registro EBP prima e poi lo parifica a ESP. Per quanto tiguarda le variabili locali, è ESP ad essere modificato, mantenendo inalterato EBP. Il C assume che le subroutine mantengono i valori di questi registri : EBX, ESI, EDI, EBP, CS,DS, SS, ES. Questo non significa che la subroutine non possa cambiarli internamente. Un'altra considerazione è che il C utilizza EBX,EDI,ESI, utilizza questi registri come registri variabili. Per ultimo ma già discusso è che per convenzione di chiamata, il valore di ritorno deve essere memorizzato in EAX, ed in ST0 viene ritornato il valore del Floating Point in questione. Il prossimo listato è simile in tutto e per tutto a precedente solamente viene fatto uso di di una sintassi tipica del C quale typedef per aumentare la leggibilità del codice. come potete vedere ora possiamo dare un nome alle variabili anzichè riferirci solamento con dei numeri. per visualizzare il valore di ritorno : debian:~/source# ./dodici [ 11223345:aabbccde:55667789:ff0099ef ] debian:~/source# echo $? 1 debian:~/source# Il valore di ritorno che avevamo messo nella funzione viene visualizzato correttamente. Ultimo esempio : ############################### ############################### ############################### .equ _dest , .equ _sorg .equ _nbit .equ _retval, 16 , , 8 -4 12 .globl _subxbit .type _subxbit, @function _subbit: pushl %ebp movl %esp , %ebp # salva ebp corrente # utilizza ebp come stack pointer #................................................... Variabili Locali subl movl $4, %esp $1, _retval(%ebp) # fai spazioni per 4 byte # inizializza la variabile locale con 1 #.................................................................... 0: xorl movl shrl %edi _nbit(%ebp) $5 movl shll addl %ecx $1 %ecx , %edi , %ecx # 1° parametro : nbit , %ecx , %edi , %edi , %edi clc movl movl _sorg(%ebp) , (%ebx,%edi,1) , %ebx %eax # 2° parametro : sorg (&sorg) movl sbbl _dest(%ebp) %eax , , %ebx (%ebx,%edi,1) # 3° parametro : dest (&dest+%edi) subl $4 , %edi loopl 0b #........................................ in %eax il valore di ritorno movl _retval(%ebp), %eax movl popl ret %ebp %ebp , %esp ENTER ENTER = Enter new stack frame Sintassi : ENTER local , nesting Questa istruzione viene messa all'inizio della sub routine, salva EBP e ne copia il contenunto nello stack, poi sottrae tanti byte quanti indicati, per far spazio alle variabili locali. ENTER 4,0 = PUSHL %EBP MOVL %ESP, %EBP SUBL $4 , %ESP nesting = 0 Utilizzato nei linguaggi quali C e fortran, che non permettono procedure annidate. nesting > 0 Utilizzato nei linguaggi quali Modula-II Ada Pascal, che permettono procedure annidate Operandi Esempi immediato ENTER 0,0 immediato ENTER 4,0 LEAVE LEAVE = lascia. E' l'opposto di enter e ripulisce lo stgack frame, leave viene eseguita prima di un a istruzione di ret. LEAVE = MOVL %ebp,%esp pop %ebp Operandi Esempio nessun argomento LEAVE C e funzioni. Ora vediamo come il C tratta le funzioni e le variabili locali, con una piccola sorpresa. immettete questo piccolo programma in C : #include <stdio.h> int Raddoppia( int val ) ; int main ( void ) { int numero = 2 ; printf ( "\n[num.: %d] \n[doppio %d]" , numero ,Raddoppia(numero) ) ; return 0 ; } int Raddoppia ( int val ) { return val*2 ; } per compilarlo e linkarlo ed eseguirlo – – – gcc prova.c -o prova ld prova ./prova compilarlo con l'opzione -S – gcc -S prova.c come risultato avrete un file prova.s. E' la generazione del codice C in assembly .LC0: .file "prova.c" .section .rodata .align 32 .string " \n [numero : %d ] \n [ doppio %d ] " .text .globl main .type main, @function main: pushl %ebp movl %esp , %ebp subl $24 , %esp andl $-16 , %esp movl $0 , %eax subl %eax , %esp #................................................................. # # -4(%dbp) = numero (variabile Locale) # 4(%esp) = num primo parametro # 8(%esp) = num*2 secondo parametro #................................................................. movl movl $2 , -4(%ebp) -4(%ebp), %eax # # inizializza la variabile locale con 2 lo passa al reistro %eax movl call %eax , (%esp) Raddoppia # pushl %eax cioè 8(%ebp) movl %eax , 8(%esp) # secondo parametro movl movl -4(%ebp), %eax %eax , 4(%esp) # primo parametro movl call $.LC0 printf , (%esp) # pushl $.LC=0 movl leave ret .size $0 , %eax main, .-main .globl Raddoppia .type Raddoppia, @function Raddoppia: pushl %ebp # salva %ebp movl %esp , %ebp movl 8(%ebp), %eax # parametro in inpout addl %eax , %eax # %eax parametro in output popl %ebp # ripristina %ebp ret .size Raddoppia, .-Raddoppia .section .note.GNU-stack,"",@progbits .ident "GCC: (GNU) 3.3.5 (Debian 1:3.3.5-8)" Commento : La direttiva .file indica che stiamo iniziando una nuovo file logico (vedi manuale) ; La direttiva .rodata marca una sezione di dati a sola lettura ; La direttiva .align allinea il segmento dati a 32. .LC0 è la variabile formato che verrà utilizzata per la chiamata alla printf. Notate che i parametri non vengono passati utilizzando la normale procedura prima descritta, ma viene allocato lo spazio necessario allo stack in testa al programma, poi vengono copiati i marametri direttamente alle locazioni di memoria, tuttavia, anche se apparentemente diverso, il risultato per quanto riguarda il passaggio dei parametri nello convenzioni di chiamata del C non cambia. Vengono passati i parametri in ordine inverso, vedesi printf. Questa tecnica decritta non prevede ri ripristinare %esp, in quanto questo viene fatto in automatico all'uscita del programma o subroutine. Per quanto riguarda la funzione Raddoppia, potete vedere il metodo standard, di passaggio parametri e di output del risultato mediante il registro %eax. Per quanto riguarda le funzioni matematiche utilizzati i numeri in virgola mobile , il valore di ritorno sarà come accennato ST0. La restante parte del programma sono delle sezioni informative per AS . and $-16,%esp Vedi L'opzione del compilatore GCC (-fomit-frame-pointer), viene utilizzata per allineare %esp, così raddoppia l'efficienza. p.s. L'istruzione MOV ed il registro %eax sono molto ottimizzati a livello harware, ecco il perchè di tale scelta. CAPITOLO 8 Stringhe e Scarpe Stringhe e Scarpe In questo capitolo tratteremo in maniera più approfondita la gestione delle stringhe e analizzando ulteriori concetti sulle librerie. Vi presento 2 file : tredici.s il programma principale e string.s che contiene due subroutine relativamente alla gestione delle stringhe. Lo scopo e' quello di costruire una libreria di stringhe e condividerla, SHARED. Come consuetudine scrivete questi file : FILE : tredici.s .include "include/stdlibc.a" .include "include/syscall.a" #################################### .section .data s1: .asciz "Nel Mezzo del cammin di nostra vita" .section .text _start: .globl _start pushl $s1 call puts addl $4,%esp pushl call addl pushl call addl $s1 _strupper $4,%esp $s1 puts $4,%esp pushl call addl pushl call addl $s1 _strlower $4,%esp $s1 puts $4,%esp xorl %ebx,%ebx SC_EXIT file : string.s ############################################ # # Converte i carateri da minusco a maiuscolo # # range caratteri 97,'a' >= 122,'z' <= # ############################################ .globl _strupper .type _strupper,@function _strupper: pushl %ebp movl %esp,%ebp 0: movl decl 8(%ebp),%ebx %ebx nop incl %ebx movb (%ebx),%al orb %al,%al jz strupper_fine # se \0 fine stringa cmpb ja cmpb jb $122,(%ebx) 0b $97,(%ebx) 0b # Cattere non ammissibile > z andb jnz $0b11011111 , (%ebx) 0b # da minuscolo a MAIUSCOLO strupper_fine: movl popl ret %ebp,%esp %ebp # carattere non ammissibile < a ############################################ # # Converte i caratteri da maiuscolo a minuscolo # # range caratteri 65,'A' >= 90,'Z' <= # ############################################ .globl _strlower .type _strlower,@function _strlower: pushl %ebp movl %esp,%ebp 0: movl decl 8(%ebp),%ebx %ebx nop incl %ebx movb (%ebx),%al orb %al,%al jz strlower_fine # se \0 fine stringa cmpb ja cmpb jb $90,(%ebx) 0b $65,(%ebx) 0b orb jnz $0b00100000 , (%ebx) # da MAIUSCOLO a minuscolo 0b strlower_fine: movl popl ret %ebp,%esp %ebp # carattere non ammissibile > Z # carattere non ammissibile < B Commento : Il programma e' composto da 2 file uno principale : (tredici.s) e uno secondario (string.s) da cui ricaveremo una libreria condivisa (SHARED LIBRARY ). Non offre particolari spunti di studio per quanto riguarda il codice sorgente ad eccezzione dell'istruzione CMP che confronta 2 operandi senza alterandone il significato. Il primo listato fa uso delle successivi funzioni presenti nella libreria string.s al fine di convertire i caratteri da minuscolo a maiuscolo e viceversa. Il secondo listato string.s presenta due funzioni _strupper e _strlower che richiedono l'indirizzo della stringa da modficare. Ricordo che per convertire da minusco a maiuscolo per quanto rigarda la codifica ASCII occorre impostare a 1 o azzerare il bit 5. quindi : da 'A' --> da 'a' --> a a 'a' 'A' --> --> or 0010:0000b and 1101:1111b imposta il 5 bit a 1 imposta il 5 bit a 0 tuttavia occorre che il carattere si trovi in un range adatto : da 65 'A' da 97 'a' --> --> a a 90 'Z' Lettere maiuscole 122 'z' Lettere minuscole al fine di evitare risultati inaspettati. Il C tratta la fine delle string con il carattere '\0' NULL , quindi quando la sub routine trova 0 termina il programma. Per convenzione personale utilizzo le etichette all'interno della subroutine premettendo il nome della subroutine stessa al fine di evitare inopportune dulicazioni di una stessa label. Ancora antepongo un underscore davanti alle funzioni per evitare conflitti con nomi della libreria C. Per esempio strlen libreria C e _strlen libreria personale, trattata più avanti. Ora e' il momendo di costruire una libreria condivisa : 1) as string.s -o string.o 2) ld -shared string.o -o libstring.so # crea il codice oggetto # crea la libreria condivisa come potete vedere per convenzione aggiungiamo lib davanti al nome e so come estensione. 3a) as tredici.s quindi... 3b) as tredici.s -o tredici.o # segnala undefined reference # ok esegue correttamente ora e' il momento di linkare il tutto 4) ld -L . dynamyc-linker /lib/ld-linux.so.2 -o tredici tredici.o -l string il comando -L . indica al linker di cercare la libreria condivisa nella directory corrente; normalmente cerca in /lib /usr/lib o in quelle definite nel file /etc/ld.so.conf il comando -l string indica di ricercare le funzioni nella libreria libstring.so 5) ./tredici ./tredici: error while loading shared libraries: libstring.so: cannot open shared object file: No such file or directory in questo caso non trova la libreria in quanto questa è stata creata in un altra directory per risolvere queto problema eseguite queti passi : 1) copiate la vostra libreria in /usr/lib o /lib oppure 2) LD_LIBRARY_PATH=. export LD_LIBRARY_PATH se questa si trova nella stessa directory 3) setenv LD_LIBRARY_PATH . C'è da dire una cosa mentre per la prima, soluzione rimane la copia nella directory per le altre al termine della sessione di bash occorrerà reimpostare le variabili. Una soluzione e' di impostare un file e tramite il comando sh reimpostare le variabili oppure modificare il file di avvio .bashrc nella directory home. CMP CMP = compare integer. Questa istruzione confronta i due operanti, sottrae il valore dell'operando sorgente a quello di destinazione, senza alterane il contenuto. Viene influenzato solo il registri EFLAGS sintassi : cmp destinazione , sorgente istruzione descrizione reg,reg cmpb %al,%bl imm,reg cmpw $FFAA,%ax reg.mem cmpl mem,%eax Condizione signed sf = of unsigned a> b zf = 0 a >= b sf = of cf = 0 a= b zf = 1 zf = 1 a <= b zf = 1 a< b sf != 0f sf != of cf = 0 cf = 1 cf = 1 zf = 0 zf = 1 File : quattordici.s .include "include/stdlibc.a" .include "include/syscall.a" #################################### .section .data s1: .asciz "123456789a" f1: .asciz "\n%d\n" .section .text _start: .globl _start pushl $s1 call puts addl $4,%esp pushl $s1 call strlen addl $4,%esp pushl pushl pushl call addl $'*' %eax $s1 _strset $12,%esp push call add $s1 puts $4,%esp xorl %ebx,%ebx SC_EXIT .data .text ################################### # # strset # # input # # carattere # ripetizioni # indirizzo stringa # ################################### .globl _strset .type _strset,@function _strset: pushl %ebp movl %esp,%ebp pushl %edi push %ds pop %es movl 16(%ebp),%eax # carattere movl 12(%ebp),%ecx # ripetizioni movl 8(%ebp),%edi # stringa cld repnz stosb popl %edi movl %ebp,%esp popl %ebp ret .data .text ################################### # # _strlen # input # # string address # # output # # eax = string len # ################################### .globl _strlen .type _strlen,@function _strlen: pushl %ebp movl %esp,%ebp pushl %ds popl %es mov 8(%ebp),%edi 0: # stringa cld xorl %eax,%eax scasb jnz 0b movl subl decl %edi , %eax 8(%ebp) , %eax %eax # ret val movl %ebp,%esp popl %ebp ret output : debian:~/source# ./quattordici.bin 123456789a ********** debian:~/source# Commento: Il listato quattordici.s ha il compito di calcolare la lunghezza effettiva della stringa e di settarne il contenuto con un carattere. Notiamo la presenza di alcune istruzioni nuove per la gesione delle stringhe. 0: cld xorl %eax,%eax scasb jnz 0b Questa parte di listato ricerca la prima occorrenza del carattere indicato dal registro %al e quando trovato continua l'esecuzione. L'istruzione stringa scasb (scan for byte) si avvale dell'ausilio di due registri supplementari che lavorano in copia %es:%edi al fine di identificare la posizione della stringa (%es deve puntare al segmento dai e %edi è l'offset della stringa). Ancora viene influenzata dal flag di direzione DF, se questo è zero (cld DF=0) viene impostata una ricerca in avanti, quindi %edi viene incrementato ad ogni passaggio. Viceversa (std DF=1) la ricerca procede a ritroso. il numero di byte sottratti o addizionati dipende dal tipo di istruzione : – – – – scasb scasw scasl scasq -> -> -> -> CLD / DF=0 STD / DF = 1 %edi %edi %edi %edi += += += += %edi %edi %edi %edi 1 2 3 4 byte byte byte byte -= -= -= -= 1 2 4 8 byte byte byte byte le istruzioni push %ds e pop %es servono ad impostare %es per puntare all'area dati. Ultimo la posizione di partenza viene messa nel registro %eax e viene eseguita una sottrazione per recuperare il numero di byte indicanti la lunghezza della stringa -1 per escludere il carattere NULL. Quindi %eax contiene l'indirizzo di rotorno. Terminana l'esecuzione del codice abbiamo in %eax l'esatta lunghezza della stringa. cld repnz stosb nella funzione _strset e' presente questa nuova istruzione stringa. Anche stosb come la precedente si avvale dell'aiuto di due registri %es:%edi su cui lavorare e del Flag di direzione per sapere se procedere avanti o indietro. In questo caso l'istruzione stos copia il byte contenuto nell'accumulutore (%al %ax %eax) all'indirizzo indicato da %es:%edi e poi procede avanti o indietro come indicato dal DF. A questa stringa viene applicato un suffisso : rep che indica di ripetere l'istruzione finchè il registro %ECX è diverso da zero. REP si basa sul registro %ECX per funzionare e ogni volta che l'istruzione viene eseguita decrementa il registro di una unità. Ovvio che se noi impostiamo %ecx con il valore di ritorno %eax chè eè la lunghezza della stringa possiamo settare con precisione la nuova stringa con il carattere voluto senza sovrascrivere il carattere terminatore NULL '\0'. SCAS SCAS = Scan For string . Ricerca la prima occorrenza del valore indicato dal registro accumulatore (%al %ax %eax) con l'operando %es:%edi. L'istruzione si avvale del registro DF al fine di decrementare o incrementare %edi. CLD STD DF=0 DF=1 ++%edi --%edi sintassi : SCAS Flag : Direction Flag DF alle istruzione scas può essere applcato il prefisso REPE o REPNE per automatizzare alcune operazioni. Spiegato nella sezione successiva in riferimento a strset.s istruzione descrizione scasb scan for byte %edi ++/-- 1 scasw scan for word %edi ++/-- 2 scasl scan for long %edi ++/-- 4 scasq scan for quad %edi ++/-- 8 STOS STOS = Store byte in string . Memorizza il valore indicato dal regitro accumulatore (%al %ax %eax), all'indirizzo indicato d %es:%edi. Si avvale del flag di direzione al fine di incrementare o decrementare il registro %edi CLD STD DF=0 DF=1 ++%edi --%edi sintassi : STOS Flag : Direction Flag DF alle istruzione scas può essere applcato il prefisso REPE o REPNE per automatizzare alcune operazioni. Spiegato nella sezione successiva in riferimento a strset.s istruzione descrizione stosb store for byte %edi ++/-- 1 stosw store for word %edi ++/-- 2 stosl store for long %edi ++/-- 4 stosq store for quad %edi ++/-- 8 REP REP = Repeat String Prefix Sintassi : REP Questa istruzione e' un prefisso di ripetizione che funziona in concomitanza con le istruzini stringa (LODS,MOVS,OUTS,SCAS,STOS ). L'istruzione viene ripetuta in base al contatore che si trova nel registro %ECX. L'indicatore ZF viene controllatore anche durante l'esecuzione di CMPS o SCAS . Se %ECX = 0 al momento che si incontra REP questa non viene ripetuta. istruzione descrizione rep ripeti se %ECX != 0 repe / repz ripeti se %ecx != 0 ripeti se gli operandi sono uguali repne / repnz ripeti se %ecx != 0 ripeti se gli operandi sono diversi Dividiamo Tutto ! La libreria di partenza string.s comincia ad essere un po' affollata sono già presenti 4 funzioni di libreria : – – – – strlower strupper strset strlen Quando dobbiamo per ovvi motivi modificare una di queste funzioni oppure inserirne un' altra, dobbiamo ogni volta ricompilare tutta la libreria, pensate a librerie molte estese con molte funzioni, risulta da un lato oneroso ricompilare in termini di tempo e dell'altro poco gestibile mantenere in codice tutto in un file. Quindi, suddivideto le quattro funzioni riportate precedentemete in altrettanti file : – – – – strlower strupper strset strlen -> -> -> -> strlower.s strupper.s strset.s strlen.s e ricompiliamo il tutti con questi comandi – – – – as as as as –gstabs+ –gstabs+ –gstabs+ –gstabs+ strlower.s -o strlower.o strupper.s -o strupper.o strset.s -o strset.o strlen.s -o strlen.o a questo punto possiamo usare due comandi : – ld -shared strlower.o strupper.o strset.o strlen.o -o libstring.so oppure – gcc -shared strlower.o strupper.o strset.o strlen.o -o libstring.so entrambi generano lo stesso risultato, la seconda opzione utilizza il compilatore GCC che come vedremo tra breve serve per linakare codice C e librerie ASM. Abbiamo così ottenuto la nostra libreria condivisa che copieremo in /usr/lib affinchè il linker possa trovarlo o in qualsiasi altro posto noi decidiamo. Risultao facile in questo modo eseguire la manutenzione del codice. Linkare libreria ASM al C Oggi molto del codice viene scritto in C,C++ è un linguaggio molto performante e ad alto livello ed a differenza del linguaggio assembler è possibile concentrarsi maggiormente sugli algoritmi di codifica, piuttosto che sulle istruzioni. Quindi risulta utile se non necessario potersi interfacciarsi con il C. inseriamo questo codice : #include <stdio.h> extern _strlen ( char *s ) ; char *str1 = "Claudio Daffra\0" ; int main ( void ) { puts ( str1 ) ; printf ("\n [%d]\n" , _strlen ( str1 ) ); } return 0 ; Questo semplice programma in C, richiama una funzione di libreria esterna come indicato extern _strlen ( char *s ) ed utilizza il codice di ritorno. Lo scopo ovvio è quello di visualizzare la dimensione della stringa. Tuttavia sorge un problema se noi compiliamo il codice C in modo usuale otteniamo un errore di compilazione in quanto il GCC eseguendo contemporaneamente Compilazione e Linking, non sa dove trovare la funzione _strlen o la libreria necessaria. – gcc -gstabs+ prova.c -> ERRORE ! Occorre compilare il programma con l'opzione -c che genera solamente il codice oggetto. – gcc -gstabs+ -c prova.c -o prova.o Ora è possibile utilizzare gcc per linkare il tutto. gcc prova.o libstring.so -o main ./main – In questo caso avviene una compilazione statica se precedentemente non abbiamo utilizzato il prefisso -shared nella generazione della libreria. Se abbiamo utilizzato -share nel linking dei moduli precedenti della libreria, e non abbiamo copiato la libreria dove il linker si aspetta di trovaralo otterremo un errore : ./main Error while loading shared library LIBSTRING.SO cannot opent share library object file no such file o directory. cp libstring.so /usr/lib – – gcc prova.o -Lstring -o main ./main Curiosita' digitate questo file : #include <stdio.h> extern _strlen ( char *s ) ; extern _strset ( char *s,int nrip,char car ) ; int main ( void ) { char *str1 = "Claudio Daffra\0" ; puts ( str1 ) ; printf ("\n [%d]\n" , _strlen ( str1 ) ); _strset ( str1,_strlen(str1),'*' ) ; puts ( str1 ) ; } return 0 ; Di particolare c'è solamente l'aggiunta dell'istruzione _strset, tuttavia genera un errore di segmentation fault; questo accade a causa del segmento .rodata che impedisce la scrittura e tratta la stringa puntatore come costante. sostituiamo : char *str1 = "Claudio Daffra\0" ; con char str1[80] = "Claudio Daffra\0" ; E avremo l'esecuzione corretta del programma. Ora vediamo la generazione del codice asm . Praticamente è cambiato tutto ! .LC0: .LC1: .file "prova.c" .section .rodata .string "Claudio Daffra" .string "" .zero 64 .string "\n [%d]\n" .text .globl main .type main, @function main: pushl %ebp movl %esp, %ebp pushl %edi subl $116, %esp andl $-16, %esp movl $0, %eax subl %eax, %esp movl movl movl movl movl movl movl movl .LC0 %eax .LC0+4 %eax .LC0+8 %eax .LC0+12 %eax , %eax , -88(%ebp) , %eax , -84(%ebp) , %eax , -80(%ebp) , %eax , -76(%ebp) #..................................copia in locale leal -72(%ebp), %edi cld movl $0 , %edx movl $16 , %eax movl %eax , %ecx movl %edx , %eax rep stosl movl $0, -92(%ebp) #...........................................puts leal -88(%ebp), %eax movl %eax, (%esp) call puts #...........................................printf leal movl call -88(%ebp) %eax _strlen , %eax , (%esp) movl movl movl movl call %eax -92(%ebp) %eax $.LC1 printf , -92(%ebp) , %eax , 4(%esp) , (%esp) #.............l #............."\n [%d] \n" #..........................................._strset movl $42 , 8(%esp) movl -92(%ebp) , %eax movl %eax , 4(%esp) leal -88(%ebp) , %eax movl %eax , (%esp) call _strset #............................................puts leal -88(%ebp) , %eax movl %eax , (%esp) call puts movl movl $0 -4(%ebp) , %eax , %edi leave ret .size main, .-main .section .note.GNU-stack,"",@progbits .ident "GCC: (GNU) 3.3.5 (Debian 1:3.3.5-8)" LEA LEA = Load Effective Address. Carica l'indirizzo effettivamente puntato dall'operando in questione. istruzione leal 4(%ebx,%esi,1),%edi corrispondenza movl %esi,%edi addl %esi,%edi addl %4 ,%edi leal var,%edi movl $var,%edi leal (%esi),%ebx movl %esi,%ebx LES LES = Load Far Pointer using ES (%ES:%EDI) istruzione lesl %edi,str corrispondenza str contenuto nel segmento dati %es %edi offset di str LDS LES = Load Far Pointer using DS (%DS:ESI) istruzione ldsl %esi,str corrispondenza str contenuto nel segmento dati %es di offset di str Simili sono le istruzioni per quanto riguarda i registri disegmento : SS,FS,GS LSS LFS LGS MOVS MOVS : Move string : copia l'operando sorgende (%ds:%esi) nell'operando desinatario (%es:%si) sintassi : movs sorgente (%ds:%esi) CLD STD Flag , , DF=0 DF=1 ++%edi --%edi : Direction Flag DF destinatario (%es:%edi) alle istruzione scas può essere applcato il prefisso REPE o REPNE per automatizzare alcune operazioni. istruzione descrizione movsb movb %ds:(%esi) , %es:(%edi) ++/-- 1 movsw movw %ds:(%esi) , %es:(%edi) ++/-- 2 movsl movl %ds:(%esi) , %es:(%edi) ++/-- 4 movsq movq %ds:(%esi) , %es:(%edi) ++/-- 8 CMPS CMPS : Compare string : copia l'operando sorgende (%ds:%esi) nell'operando desinatario (%es:%edi) sintassi : cmps sorgente (%ds:%esi) CLD STD Flag , , DF=0 DF=1 ++%edi --%edi : Direction Flag DF destinatario (%es:%edi) alle istruzione scas può essere applcato il prefisso REPE o REPNE per automatizzare alcune operazioni. istruzione descrizione cmpsb cmpsb %ds:(%esi) , %es:(%edi) ++/-- 1 cmpsw cmpsw %ds:(%esi) , %es:(%edi) ++/-- 2 cmpsl cmpsl %ds:(%esi) , %es:(%edi) ++/-- 4 cmpsq cmpsq %ds:(%esi) , %es:(%edi) ++/-- 8 LODS LODS : Copia nell'accumulatore (%al,%ax,%eax) una valore dalla memoria prelevato da (%ds:esi) sintassi : CLD STD Flag lods (%ds:%esi) , %al DF=0 DF=1 ++%edi --%edi : Direction Flag DF alle istruzione scas può essere applcato il prefisso REPE o REPNE per automatizzare alcune operazioni. istruzione descrizione lodsb movb %ds:(%esi) , %al lodsw movw %ds:(%esi) , %ax lodsl movl %ds:(%esi) , %eax lodsq movq %ds:(%esi), %rax (registro a 64 bit) riepilogo stosb %al , %es:(%edi) lodsb %ds:(%esi) , %al movsb %ds:(%esi) , %es:(%edi) cmpsb %ds:(%esi) , %es:(%edi) scasb %es:(%edi) CAPITOLO 9 Impariamo il C Impariamo il C Non e' proprio quello che volevo dire ma..., in questo capitolo vengono presentate le strutture di controllo, tipiche del linguaggio C, rapportate all'assembly. Variabili il C dispone di vari tipi di variabili nel quale memorizzare i dati, analogamente GAS dispone di tipi simili dove ricavare le dimensioni per le variabili. C GAS char .char un/signed char .byte un/signed int .int un/signed long .long float .float double .double char * .ascii .string int *pointer = &var .long esempio : unsigned int intero = 1 ; int *pintero = &intero ; intero: pintero: .int 1 .long intero gli indirizzo vengono memorizzati in 32 bit. Quindi il puntatore 'pintero' contiene l'indirizzo della variabile 'intero'. movl intero ,%eax # %eax = 1 movl pintero,%eax # %eax = $intero ; (%eax) = 1 pointer .macro pointer nome indirizzo \nome: .long \indirizzo .endm string .macro string nome init \nome: .string "\init" .endm esempi : var def pippo 10 pointer ppippo pippo string str1 “\nSalve a tutti!” ########################## CONST .macro const .section .rodata .endm ########################## VAR .macro var .section .data .endm ########################### DEF / LET # ########################### .macro def tipo nome init \nome: .\tipo \init .endm ######################### STRING .macro string nome init \nome: .string "\init" .text .endm ######################## POINTER .macro pointer nome indirizzo \nome: .long indirizzo .endm esempio : ################################# const string str1 "ciao\0" ################################ var def let int int j i 0 0 pointer pi i Commento : L'esempio precedente prende in considerazione l'utilizzo di due importanti clausole per quanto riguarda la gestine dei dati del compilatore .data e .rodata, la prima e' stata già trattata, la seconda memorizza i dati in un segmento di sola lettura, ogni tipo di accesso in scrittura provochera un 'segmentation fault' La protezione dei è un ottimo punto di partenza per una buona programmazione. Le macro 'const' e 'var' sono solo dei nomi che specificano un riferimendo '.section. rodata' oppure '.section .data' null'altro. Le variabili o constanti di uso più comune ad eccezione delle stringhe e' possibile definirle con la macro 'def' oppure 'let' tipica del vacchio basic. Il valore di inizializzazione e' facoltativo, in quanto per default esse vengo inizializzate a zero. E' tipico di una cattiva programmazione non inizializzare le variabili (a meno che non si tratta di tipo 'volatile'). typedef Nel linguaggio C vegono utilizzati dei tipi predefiniti, per aumentare la leggibilità del programma. typedef UINT unsigned int Cosi in assembly e' possibile ottenere questo artifizio con la clausola .equ .equ UINT unsigned in teoria funziona, ma in pratica, GAS non riesce a sostituire il simbolo nella variabile ed ad interpretalo, prendiamo in considerazione la seguente macro : .macro x var \var ... .endm .equ num , $10 x pippo Gas non sostituirà $10 a var, ma questa variabile assumerà il valore della stringa “var”. FOR ... NEXT In realtà questa istruzione fa più riferimento al vecchio basic, che non al linguaggio C,tuttavia mi piaceva l'idea e l'ho mantenuta. Nella sintassi del Basic l'istruzione relativa al ciclo e' questa : FOR VAR = from TO to STEP inc NEXT VAR Ovviamente, il significato è palese; simile è quella in C : for (var = from ; var <= to; var += inc) { } cosi' possiamo rapportare tale istruzione al linguaggio assembler Esempi : for diretto for indiretto movl FROM,%eax for: movl $FROM,var cmpl $TO,var jg next movl %eax,var for: movl TO,%eax cmpl %eax,var jg next ... next: ... addl $inc,var jmp for movl inc,%eax addl %eax,var jmp for next: MOVL $FROMI,I FORI: CMPL $TOI,I JG NEXTI MOVL $FROMJ,J FORJ: CMPL $TOJ ,J JG NEXTJ FOR I = $FROMI TO $TOI STEP INCI FOR J = $FROMJ TO $TOJ STEP INCJ ... ... NEXTJ: ADDL $INCJ,J JMP FORJ NEXT J ... NEXTI: ADDL $INCI,I JMP FOR NEXT I Non si trova nei comuni programmi in assembly lo stile di scrittura dei tipico di quelli ad alto livello, tuttavia si migliora sensibilmente la lettura del sorgente, che non una serie di istruzioni in modo sequenziale. Fate Vobis. Essendo di natura routinaria, risulta comodo costruire una macro di riferimento per il ciclo : sintassi generale : FORL name , var, da, a, (passo) NEXTL name name = nome del ciclo per renderlo univoco var = variabile di controllo del ciclo (precedentemente definita) I: .long 0 da = valore inizio ciclo a = valore fine ciclo passo = incremento (per default=1) NEXTL name name = nome del ciclo Questa e' una forma generale con delle variabili di tipo long, facilmente adattabile anche ai registri e quindi modificabile per quanto riguarda byte, word ecc... ############################ # ############################ FORL .macro forl name var from to inc=$0x01 pushl \from popl \var decl \var for\name: addl \inc,\var cmpl jg \to,\var next\name .endm ############################ NEXT # ############################ .macro next name jmp for\name next\name: .endm ############################ esempi : for variabili for registri forl ciclo1 i $1 $5 $1 forl ciclo2 j $1 $5 $1 forl ciclo1 i $1 $5 forl ciclo2 %eax $1 $5 pushl %eax ... next ciclo2 next ciclo1 ... popl %eax next ciclo2 next ciclo1 Ovviamente andranno salvati i registri, che si presume vadano distrutti da altre subroutine. L'ultima variabile se l'incrementeo specifico è 1 può anche essere omessa e non passata diretamente alla macro, in quento viene auto assegnata. Questa macro di natura generale tratta i numeri con segna di tipo long. Per i numeri senza segno occorre sostituire JG con JA. BREAK / CONTINUE / EXIT L'istruzione break del C, termina il blocco interno di esecuzione di una istruzione, per uscire al più esterno. Se in un ciclo for ... il programma in trovaa break, terminerà l'esecuzione del ciclo per passare a altro. Contrariamente continue ritornerà alla fase iniziale del ciclo incrementando il contatore senza, “continuare” ad eseguire quello che c'è nel blocco. E' possibile ottenere questo con due semplici macro che poi non sono altro che due istruzioni di salto: ex break continue for (int i=0; i<10; ++i) { for (int i=0; i<10; ++i) { if ( ? ) break ; /* termina ciclo */ if ( ? ) continue ; /* ritorna a for */ } } BREAK NEXT / CONTINUE FOR / EXIT FOR break next continue for exit for .MACRO BREAK \TIPO \NOME JMP \TIPO\NOME .ENDM .MACRO CONTINUE \TIPO \NOME JMP \TIPO\NOME .ENDM .MACRO EXIT \TIPO \NOME JMP \TIPO\NOME .ENDM Le macro sono praticamente identiche cambia il nome solo per un fatto di leggibilità. BREAK NEXT / CONTINUE FOR break next continue for forl ciclo1 i $1 $5 break next ciclo1 forl ciclo2 %eax $1 $5 pushl %eax forl ciclo1 i $1 $5 continue next ciclo1 forl ciclo2 %eax $1 $5 pushl %eax ... popl %eax next ciclo2 next ciclo1 i=1 ... popl %eax next ciclo2 next ciclo1 i=6 IF ... THEN ... ELSE ... ENDIF Nel linguaggio BASIC / C e nei linguaggi di più alto livello sono presenti le comode istruzioni di “condizione”, le quali al verificarsi della stessa eseguono un blocco vero oppure falso. se %eax > %ebx allora cmp %eax,%ebx jng endif ... vero endif: in questo esempio alla condizione iniziale greater 'g' va aggiunto il suffisso 'n' not, in quanto la condizione viene eseguita solo se '>' appunto. Diversa e' la situazione per l'utilizzo della clausola 'altrimenti' 'else' in quando il blocco 'vero' viene utilizzato senza not se %eax > %ebx allora ... altrimenti cmp %eax,%ebx jng elseif ... vero jmp endif elseif: ... falso endif: l'utilizzo delle macro, non nella programmazione delle stesse rende tutto più semplice. Come potete vedere nell'esempio successivo. La mia personale collezione di macro riguardanti le strutture di controllo C/BASIC le ho messe nel file basic.a- ######################## IF ... THEN ... ELSE ... ENDIF # ######################## .macro if name op1 cnd op2 then="else" if\name: cmp \op1,\op2 jn\cnd nothen\name j\cnd \then\name nothen\name: .endm .macro else name jmp endif\name then\name: .endm .macro endif name else\name: endif\name: .endm Esempio : movl movl $1,%eax $2,%ebx if c1 %eax g %ebx then C_PUTS " %eax > %ebx " else c1 C_PUTS " %eax < %ebx " endif c1 if c2 %eax g %ebx C_PUTS "\n %eax < ebx \n" endif c2 a queste istruzioni ovviamente e' possibile associare : – – break if (name) continue if (name) la prima istruzione esce dalla condizione, la seconda insolita ripete la condizione iniziale, e' facile ottenere un loop infinito se non si raggiunge la condizione di uscita. IF ... AND ... OR THEN Nelle comuni operazioni di condizione appaiono per semplificare il codice, l'utilizzo degli operatori booleani AND OR e NOT, i quali combinati danno luogo ad un'unica condizione. In asembly non e' possibile mischiare questi operatori in un unica istruzioni, ma tramite l'utilizzo di più condizioni arriviamo ugualmente allo scopo. Vedi esempio : IF ( (( A > B ) && ( A < C )) ||( C==B) ) { ... } Questo blocco di istruzioni è tratto dal linguagio 'C', ed il blocco interno viene eseguito dal verificarsi delle condizioni. Per ottenere l'effetto desiderato in assembly scomponiamo il tutto, in una prima parte AND e poi OR. ifor: cmp jng a,b ifor cmp jl a,c iftrue cmp c,b jne iffalse #1 se a < b vai a or #2 se anche a < c allora vera #3 se c!=b allora falso iftrue: ... jmp endif iffalse: ... endif: #1 se la prima condizione è falsa essendo una AND anche la seconda di conseguenza sarà falsa. #2 se anche la seconda istruzione è vera, il primo blocco è verificato; di conseguenza l'istruzione OR non ha più influsso sul codice. #3 arrivati alla terza condizione, essendo le prime due false, non mi resta che virificare la OR appunto. Se questa risulta falsa, l'intera condizione è falsa altrimenti risulta vera, anche se la precedente ha dato risultato negativo: Il linguaggio 'C' normalmente non esegue tutte le condizioni, ma verifica le prime ed in cascata le altre, come abbiamo potuto vedere dalla traduzione della condizione AND. Ora vediamo come il C ottimizza le condizioni. if ( (a>b) && (a>c) && (a>d) ) { ... } Possiamo notare che la sola condizione “falsa” di un AND portà a falsare l'intera condizione. Traduzione assembly : cmp a,b jng iffalse #1 esci se falso ... cmp a,c jng iffalse cmp a,d jng iffalse iftrue: ... jmp endif iffalse: ... endif un accorgimento per velocizzare la condizione, è calcolare a tavolino l'utilizzo effettivo delle singole variabili. Per esempio se la variabile c viene usata molto spesso ed ha una certa importanza nella condizione, allora spostiamola in prima posizione, così eviteremo di eseguire più confronti. IF OR ... NOT La codifica OR poi non è dissimile dalle altre, vengono fornite due possibili soluzioni, if ( (a>b) ||!(a>c) ) {...} traduzione : cmp setg a,b ah cmp a,b jg iftrue cmp a,c setng al cmp a,c jng iftrue or jz ah,al iffalse iftrue: iftrue: ... ... jmp endif jmp endif iffalse: iffalse: ... endif: ... endif: In riferimento al primo esempio è possibile utilizzare il linguaggio delle macro precedentemente descritto : IF ( (( A > B ) && ( A < C )) ||( C==B) ) { ... } if cndab if a g b then cndac a l c then ... exit endif cndab else cndac #.....................se le AND risulato false, vai per la OR continue else cndab endif else cndab if cndcb c e b ... exit endif cndad endif cndcb endif cndab Le istruzioni BREAK, CONTINUE, EXIT : non sono nient'altro che 'jmp' in questo caso ho utilizzato EXIT al posto di BREAK , in quanto break significherebbe di uscire dal ciclo più interno a quello più esterno; mentre EXIT è più approppriato per uscire dalla IF. Nella costruzione delle macro ho lasciato molta libertà ai comandi BREAK CONTINUE EXIT, tuttavia devono essere messi in modo non approppriato, o in breve tempo si creerà un programmazione a “SPAGHETTI ”. CICLI Abbiamo visto il ciclo FOR NEXT, tuttavia ne esistono di altri tre tipi a con associato un confronto, in relazione al tipo di ciclo questi sono : – – – WHILE ... REPEAT ... BEGINLOOP ENDWHILE UNTIL ... ENDLOOP WHILE / ENDWHILE Questo tipo di ciclo esegue l'istruzione al suo interno, 'MENTRE' la condizione iniziale è verificata, altrimenti esce. # WHILE ... ENDWHILE # WHILE ... ENDWHILE (assembly) (macro) while: .macro while name op1 cnd op2 while\name: cmp op1,op2 jng endwhile ... jmpwhile cmp \op1,\op2 jn\cnd endwhile\name .endm .macro endwhile name jmp while\name endwhile: endwhile\name: .endm REPEAT / UNTIL Questo tipo di ciclo esegue l'istruzione al suo interno, finchè non si verifica la condizione. Esce quando la condizione è vera. REPEAT ... UNTIL REPEAT ... UNTIL (assembly) (macro) repeat: .macro repeat name repeat\name: ... .endm .macro until name op1 cnd op2 cmp op1,op2 jng repeat cmp \op1,\op2 jn\cnd repeat\name .endm LOOP / ENDLOOP Questo tipo di LOOP è legato al registro ECX, vedi istruzione loop per maggiori chiarimenti loop assembly loop macro movl $100,%ecx startloop: ... # LOOP ... ENDLOOP .macro loop name count movl \count,%ecx loop startloop loop\name: pushl %ecx esempio : .endm loop ciclo1 10 .macro endloop name popl %ecx ... endloop loopnz loop\name .endm Sono dei cicli molto semplici da rappresentare, e non richidono siegazioni, in quanto chiari nella propria esposizione, notate solamente il salvataggio del registro %ecx affincè non subisca modifiche dalle varie operazioni all'interno del ciclo. La macro “loop” prende il nome come l'istruzione stessa, GAS va prima a ricercare le macro e poi una volta effettuate le opportune sostituzioni interpreta il contenuto e quindi i comandi. SWITCH Esempio Macro movl $3,%ecx ########################## switch switch secx .macro switch name switch\name: case %ecx,$1,caso1 .endm case %ecx,$2,caso2 case %ecx,$3,caso3 ########################## case .macro case var value st default cmp \value,\var C_PUTS "\ndefault\n" je \st break endswitch secx .endm caso1: ######################### default C_PUTS "\ncaso1\n" .macro default break endswitch secx #default .endm caso2: C_PUTS "\ncaso2\n" break endswitch secx ######################## endswitch .macro endswitch name endswitch\name: caso3: C_PUTS "\ncaso3\n" break endswitch secx endswitch secx .endm Per prima cosa ho fornito direttamente l'esempio dell'istruzione switch, nel linguaggio con questa istruzioni di alto livello, esegue una serie di confronti atta a determinare il valore della variabile di partenza indicata da switch, al fine di eseguire una data operazione. Esempio : switch ( c ) { case 1 : ... break ; case 3 : ... break ; case 5 : ... break ; }; default: ... break ; Tuttavia questo tipo di istruzione è molto lenta in quando se nell'esempio precedente la variabile c è 5 dovrà fare 3 confronti, oppure se la variabile è out of range, cioè al di la' dei limiti dello switch cioè minore di 1 oppure maggiore di 3, dovrà comunque eseguire tutti i controlli. la medesima codifica in assembly, risulta più potente in quanto si possono inserire più variabili per il confronto oppure inserire delle condizioni di uscita allo scopo di evitare inutili confronti. macro macro migliorata switch sc switch sc case c,$1, caso1 case c,$3, caso3 case c,$5, caso5 default ... break endswitch sc caso1: ... break endswitch sc caso3: ... break endswitch sc if $1 l c exit endswitch sc endif if $5 g c exit endswitch sc endif default case c,$1, caso1 case d,$1, caso1 case c,$3, caso3 case c,$5, caso5 ... caso5: ... break endswitch sc endswitch sc endswitch sc Espressioni booleane In parte questo procedimento l'abbiamo già visto , ma per esteso vi riporto una sezione di codice che fa uso degli operatori booleani : bool = ( (v1==v2) && (a<=d)) ||(eta != 15) ) movw v1,%ax cmpw v2,%ax sete %al # se v1 = v2 allora al = 1 movw a,%bx cmpw d,%bx setle %bl # se a <= d allora bl = 1 and # bool = %al and %bl bl,al mov ax,eta cmp ax,5 setne al # se eta != 5 allora al = 1 or bl,al # bool = %bl or %al movl B,b1 Questo esempio funziona su di un processore dal .386 in su, in quanto fa uso delle istruzioni set. bool = ( (eta!=15) ||(v1==v2) && (a<=d)) ) movw $1,bool# setta l'espressione come vera cmpw $15,eta # se eta != 15 allora espressione vera jne esci ... esci: movl $0,bool ret Grazie alla proprietà commutativa delle operazioni, possiamo invertire la sequenza prima esposta in questo mondo, così nal migliore dei casi velocizzare l'espressione booleana ad un solo confronto, avendo già settato l'espressione come vera. Switch un'altra volta listato : quindici.s .include "include/stdlibc.a" .include "include/syscall.a" .text _start: .globl _start jmp switch sst1: ###################### sst1 ret sst2: sst3: C_PUTS "\n sst1 " ###################### sst2 C_PUTS "\n sst2 " ret ###################### sst3 C_PUTS "\n sst3 " ret ###################### default default: C_PUTS "\n default " ret ###################### switch data .data valuetab: .long 3,default .long 10,sst1 .long 20,sst2 .long 30,sst3 #################### switch code switch: cicla: salta: esci: movl movl movl cmpl je loopnz $10,%ebx (valuetab),%ecx valuetab(,%ecx,8),%eax %eax,%ebx salta cicla # valore da confrontare # valore iniziale tabella # carica con il primo valore call *valuetab+4(,%ecx,8) # se uguale vai allo statemente indicato dalla tabella # se non trovi niente vai allo statement di default movl $0,%ebx C_EXIT # se uguale vai allo statemente indicato dalla tabella commento : Il programma qundici.s mostra un ulteriore esempio di come ottenere l'istruzione switch. Meno elegante, ma sicuramente più incisiva. Una particolarità del programma è che miscela i segmenti codice e dati insieme, questo per noi non e' un problema, in quanto .text e .data sono delle direttive per raggruppare le sezioni .section; Questo tipo di approccio serve solo a rendere più leggibile il programma il quale fa riferimento ad una tabella da cui prelevare informazioni per la switch, quali : il numero di case, i valori, le label a cui fare riferimento per i salti e le label di default nel qual caso nessuna operazione di contronto è andata a buon fine. Vedi tabella “valuetab” valuetab: valori indirizzo statemente indirizzo 3 valuetab+0 default valuetab+4 10 valuetab+8 st1 valuetab+12 20 valuetab+16 st2 valuetab+20 30 valuetab+24 st3 valuetab+24 – – ( valuetab + 0 ) contiente il numero di valori da valutare ( valuetab + 4 ) contiene l'indirizzo della label di default Nel nostro esempio il registro %ebx contiene il valore da comparare con gli altri. Il registro %ecx contiene il numero di valori da comparare, quindi la comparazione viene fatta dall'ultimo elemento al primo. Sfruttando le proprietà di loop possiamo risparmiare qualche byte, per decrementare %ecx e quindi passare al successivo (precedente) elemento, ed arrivare nel caso in cui nessun valore sia uguale a %ebx a %ecx = 0, che punta porprio alla label di default, in quanto moltiplicando l'indice per 8 (.quad) risulta sempre zero. Se la comparazione ha esito positivo viene eseguito una call indiretta con i valori approppriati per puntare alla label corretta. Nel nostro caso la gestione eram molto semplice in quanto i valori erano tutti uguali (.long) cioè 4 byte, ma se prendiamo byte,oppure stringhe o altro la situazione si complica, io preferisco dividere i valori e gli indirizzi in 2 distinte tabelle. Dal canto mio non prediligo il primo o il secondo metodo, tuttavia il primo esempio di switch a mio avviso risulta più intuitivo, il secondo certo più efficace. State Machine Nell'esempio precedente potevamo sostituire alla call una semplice JMP, e sostituire ret con jmo endswitch, è indifferente l'utilizzo di uno oppure di un altro metodo, tuttavia a volte risulta comodo mantenere l'indirizzo in un parametro da passare all'istruzione jmp. come potete vedere la subroutine tiene traccia della sua esecuzione, e cambia ogni volta che viene chiamata l'indirzzo di esecuzione. Questo nel linguaggio assembly viene fatto con il salto indiretto. stato_macchina: .long label1 label1: label2: label3: jmp stato_macchina ... mov ret $label2,stato_macchina ... mov ret $label3,stato_macchina ... mov ret $label1,stato_macchina Dulcis in fundu ! Vediamo ora come costruire dei bit di struttura. Bisogna porre la massima attenzione agli assegnamenti alle posizioni, in quando occorre far riferimeno alle operazioni logiche molto frequentemente, ed un banale ereore va a compromettere l'integrità del dato. Vediamo il listato di esempio : .include "include/syscall.a" .include "include/stdlibc.a" .data ############################################################# # # struct spippo bit pos. rif. # { # unsigned a:1 ; 0 1 1 # unsigned b:2 ; 1 2 24 6 # unsigned c:4 ; 3 4 5 6 8 16 32 64 # unsigned d:1 ; 7 128 128 # } pippo ; # ############################################################ .data pippo: # dccccbba .byte 0b01010101 .text _start: .globl _start #..................pippo.a = 0 leal pippo ,%eax andb $-2 ,(%eax)#0x1111:1110 #..................pippo.d = 0 leal pippo ,%eax andb $127 ,(%eax)#0x0111:1111 #..................pippo.d = 1 leal pippo ,%eax orb $-128 ,(%eax)#0x1000:0000 #..................pippo.c = 8 xorl %eax ,%eax movb pippo ,%al andl $-121 ,%eax orl $64 ,%eax movb %al,pippo fine: xorl %ebx,%ebx C_EXIT # 0x1000:0111 azzera # 0x010000000 64 relativo a 8 120 commento : Allora bisogna iniziare la discussione, dalla definizione della struttura, che ingloba in un solo byte, 4 variabili espresse in bit : struct spippo { unsigned unsigned unsigned unsigned } pippo ; a:1 b:2 c:4 d:1 ; ; ; ; bit pos. rif. 0 1 2 3 4 5 6 8 7 1 24 16 32 64 128 1 6 120 128 Le variabili a, b, c, d occupano uni spazio in memoria esattamente di 1 2 4 1 bit! la somma dei bite equivale esattamente ad un byte! Ogni Bit occupa una determinata posizione all'interno del byte : – – – – a occupa il bit 0 ; b occupa i bit 1 2 ; c occupa i bit 3 4 5 6 ; d occupa il bit 7 Univocamente è possibile riferirci alle simgole posizioni con dei valori, che il bit a 1 in binario rappresenta l'esatta posizione della variabile di riferimento : – – – – a occupa il bit 0 b occupa i bit 1 2 c occupa i bit 3 4 5 6 d occupa il bit 7 0x0000:0001 1 0x0000:0110 6 0x0111:1000 120 0x1000:0000 128 Quindi una volta ottenute le posizioni possiamo giocare con le istruzioni logiche per modificare solo un area ristretta del byte. La dichiarazione della stuttura avviene nel seguente modo, non differisce poi da quella di una variabile. in queto caso sono state inizializzate le singole variabili : a1 b 01 c 0101 d0 pippo: # dccccbba .byte 0b01010101 # struct spippo pippo ; Ora vediamo come manipolare la variabile a,inserendo un valore di zero : #..................pippo.a = 0 leal pippo,%eax andb $-2,(%eax) #0x1111:1110 Carico l'indirizzo effettivo della variabile pippo, che contiene il byte della struttura, e lo metto in %eax, a questo azzero il bit numero 0 indicante la posizione di a (0x1111:1110). Non dissimile è la parte teorica/pratica per la variabile 'd'; trovandosi in 7^a posizione dovrò riferimi con 127. #..................pippo.d = 0 leal pippo andb $127 ,%eax ,(%eax)#0x0111:1111 E cosi si procede per la variabile d, il prossimo esempio è un pochino più complesso relativo alla variabile d. #..................pippo.c = 8 xorl movb andl orl movb %eax pippo $-121 $64 %al , %eax , %al , %eax , %eax , pippo # 0x1000:0111 azzera # 0x010000000 64 relativo a 8 in questo esempio (adattabile alla variabile b, azzeriamo %eax, carichiamo in memoria il byte, che ricordo è l'unita più picola indirizzabile dall'x86, ne azzero la parte centrale quella relativo ai 4 bit della variabile 'c' e inserisco il valore 8 nel fomato binario 8 = 0x0000:1000 dovendolo adattare alla posizione 4 5 6 7 questo diventa 0100:0000 appunto 64. Chiaramente questa è una gestione molto semplicistica delle sturtture di bit; strutture molto complesse o riferimenti ad essa richiedono parecchia attenzione, di mio avviso ringrazio il compilatore GCC per svolgere in autonomia ed alla perfezione questo lavoro. Osservate questo esempio : // prova.c #include <stdio.h> .type main, @function main: int main ( void ) pushl %ebp { movl %esp, %ebp struct spluto { subl $40, %esp unsigned a:12 ; andl $-16, %esp unsigned b:2 ; movl $0, %eax unsigned c:31 ; subl %eax, %esp unsigned d:31 ; movl -24(%ebp), %eax } pluto ; movw %ax, -40(%ebp) movl -40(%ebp), %eax pluto.a = 8 ; andl $-4096, %eax pluto.b = 4 ; orl $8, %eax pluto.c = 5 ; movw %ax, -40(%ebp) pluto.d = 12 ; movl -40(%ebp), %eax movw %ax, -24(%ebp) return 1 ; } movzbl-23(%ebp), %eax andb $-49, %al movb %al, -23(%ebp) movl -20(%ebp), %eax andl $-2147483648, %eax orl $5, %eax movl %eax, -20(%ebp) movl -16(%ebp), %eax andl $-2147483648, %eax orl $12, %eax movl %eax, -16(%ebp) movl $1, %eax leave ret Questa è la rispettiva codifica, che GCC esegue, dell'esempio del programma scritto in C. Buona Fortuna ! CAPITOLO 10 Non Ricordo, ho bisogno di MEMORIA ! Non Ricordo, ho bisogno di MEMORIA ! Questo capitolo tratta, la gestione della memoria da parte di linux, nonchè della trattazione degli array, indici e come allocare e deallocare la memoria della macchina. Iniziamo con l'utilizzare un semplice array, listato : diciassette.s .include "include/syscall.a" .include "include/stdlibc.a" .include "include/basic.a" ###################### array[10] .bss .lcomm array,10 * 4 var def long string i 0 str1 "\n[ %d ]\0" .text .globl _start _start: #....................... inizializza l'array forl a1 i $0 $10 movl i,%ebx # indice addl %ebx,%ebx addl %ebx,%ebx # moltiplica per 4 (long) movl i,%eax # memorizza valore movl %eax,array(%ebx) next a1 xorl %ebx,%ebx C_EXIT Commento : Questa parte di programma è molto semplice, ha il solo scopo di introdurre alcuni concetti basilari per quanto riguarda la trattazione degli array. Riprendiamo il discorso della sezione .bss; questa viene utilizzata al fine di allocare risorse in runtime, e non per accrescere le dimensioni del file. La nuova direttiva da tener in considerazione è .lcomm che serve per allocare una determinata quantità di byte. Nel nostro caso dovendo allocare un array di .long di dieci elementi, questo equivale a (40 byte). La variabile %ebx contiene l'indice iniziale, che andrà moltiplicato per 4 appunto un long. Le due istruzioni seguenti addl, simulano perfettamente l'istruzione mul e sono notevolmente più veloci. (moltiplicare per 4 significa sommare due volte lo stesso numero). La restante parte del programma, fa riferimento a un indirizzamento basicizzato indiretto, array locazione iniziale e %ebx offset. Strutture e Array Inserici il file : diciassette.s .include "include/syscall.a" .include "include/stdlibc.a" .include "include/basic.a" .section .data var def long i 0 string str1 "\n[ %d ]\0" ################################# struct # # struct simpiegato # { # char cognome[20] ; # char nome[20] ; # long id ; # } # ############################### .equ simpiegato_cognome , 0 .equ simpiegato_nome , 20 .equ simpiegato_id # 20 # 20 , 40 # .equ simpiegato_size , 44 # 44 4 ############################### # # struct simpiegato aimpiegato[5] # ############################### .data .section .bss .lcomm aimpiegato, simpiegato_size 5 .data string initnome "*******************\0" string initcogn "...................\0" ############################### .text .globl _start _start: pushl %ebp movl %esp,%ebp #............... inizializza l'array forl a1 i $0 $4 movl i ,%eax movl $simpiegato_size,%ecx mul %ecx # indice iniziale ################################# aimpiegato_id movl $aimpiegato ,%ebx # base addl %eax ,%ebx # indice addl $simpiegato_id ,%ebx # offset movl $0x1234,(%ebx) ################################# aimpiegato_cognome movl $aimpiegato ,%ebx # base addl %eax ,%ebx # indce addl $simpiegato_cognome ,%ebx # offset pushl pushl pushl call addl popl %eax $initcogn %ebx strcpy $8,%esp %eax ############################### aimpiegato_nome movl $aimpiegato ,%ebx addl %eax ,%ebx addl $simpiegato_nome ,%ebx pushl pushl pushl call addl popl %eax $initnome %ebx strcpy $8,%esp %eax next a1 xorl %ebx,%ebx C_EXIT commento : Questo programma tratta la gestione, del tipo “struct” in C e delle stesse come array. Questo programma non fa nulla a parte inizializzare l'array di stutture. struct simpiegato { char cognome[20] ; char nome[20] ; long id ; } ; La struttura in c può essere scritta in questo mondo con GAS (diversamente con altri tipi di assembler) : .equ .equ .equ .equ simpiegato_cognome , 0 simpiegato_nome , 20 simpiegato_id simpiegato_size # # , , 20 20 40 44 # 4 # 44 praticamente entrambi i costrutti, non fanno nient'altro che definire le dimensioni dei dati. In Gas Assembly questo è un po' più complicato in quanto occorre definire i dati non tanto in base alla propria lunghezza, quanto in base alla loro posizione all'interno della struttura, inoltre occore conoscere quanto le dimensioni della struttura, 0 20 40 sono le posizioni dei dati all'interno della struttura e 44 è la dimensione della struttura. struct simpiegato aimpiegato[5] ; .section .bss .lcomm aimpiegato, simpiegato * 5 La definizione della strutura non è poi tanto diversa da quanto lo era quella dell'array, il tipo long che avevamo definito andrà sostituito con l'attuale lunghezza della struttura, quindi viene allocata 5 volte la dimensione della struttura. la variabile 'i' viene utilizzata come indice e come per quanto rigurda l'array è utilizzata per calcolare la posizione iniziale della struttura all'interno dell'array. movl i,%eax movl $simpiegato_size,%ecx mul %ecx # indice iniziale A questo punto %eax contiene la posiziono iniziale della struttura numero 'i'. Occorre a questo punto accedere agli elementi movl $aimpiegato ,%ebx addl %eax ,%ebx addl $simpiegato_cognome ,%ebx %ebx contiene la posizione in memoria dell'array di struttura %eax contiene la posizione indicata dall'indice 'i' A questi andrà sommato l'effettivo offset dell'elemento a cui vorremo accedere. Appunto $simpiegato_cognome zero nel nostro caso. In definitiva il registro %ebx conterrà la posizione dell'elemento dell'arrray di struttura indicato dall'indice 'i'. Come dicevo alcuni tipi di assemblatori, contemplano al loro interno già il tipo struttura, e consentono di accedere con maggior eleganza e rapidità che non con questo metodo. Tuttavia in questo modo si ha la padronanza dell'intero processo. Precedentemente ho definito un piccolo linguaggio di macro, ma ricordo che lo scopo di questo libro è imparare l'assembler e non il linguaggio macro. Risulta comodo in operazioni ripetitive poter disporre di macro al fine di meglio manipolare frammenti di codice e liberarci da operazioni tediose e spesso contorte per arrivare alla soluzione dell'algoritmo. Sherlock Holmes in arte Il Signor DEBUG ! Nell'esempio precedente, il programma non genera alcun risultato, visibile, tranne che inizializzare l'attuale struttura. Era possibile definire delle printf per vedere il contenuto delle singole locazioni di memoria, tuttavia disponiamo di uno strumento molto potente per poter vedere e manipolare tutto il programma con la lente d'ingrandimento. Il programma in questione 'GDB, Gnu Debugger. Nella costruzione dei programmmi e ancor più di veri progetti, e non di progetti giocattolo, risulta fondamentale poter disporre di uno strumento che tenga traccia di ogni cosa all'iterno del nostro programma, poter visionare la memoria e le singole istruzioni, passo per passo al fine di rilevare eventuali errori o altro nel nostro codice. Prima di tutto occorre ricompilare il codice aggiungento questa opzione alla riga di comando : – – as –gstabs+ gcc -gstabs+ ( 1 ( due underscore ) underscore ) Questa opzione consente di aggiungere delle informazioni programma, al fine di poter essere utilizzate con gdb. in più al nostro Una volta compilato il programma digitate : debian:~/source# gdb diciassette oppure debian:~/source# ddd diciassette (modalità grafica dello stesso) Copyright 2004 Free Software Foundation, Inc. GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions. Type "show copying" to see the conditions. There is absolutely no warranty for GDB. Type "show warranty" for details. This GDB was configured as "i386-linux"...Using host libthread_db library "/lib/tls/libthread_db.so.1". (gdb) questa è la schermata iniziale di presentazione del programma, (gdb) list 1 2 3 4 5 6 7 8 9 10 (gdb) .include "include/syscall.a" .include "include/stdlibc.a" .include "include/basic.a" .section .data var def long i 0 Tramite il comando list possiamo vedere il listato del nostro programma, non ancora inesecuzione. list riga iniziale,riga finale. (gdb) disassemble _start Dump of assembler code for function _start: 0x080481cc <_start+0>: push %ebp 0x080481cd <_start+1>: mov %esp,%ebp 0x080481cf <_start+3>: push $0x0 0x080481d1 <_start+5>: popl 0x8049248 0x080481d7 <_start+11>: decl 0x8049248 End of assembler dump. (gdb) E' possibile ottenere un disassemblato del programma, a partire dalla locazione desiderata, in questo caso la visualizzazione si interrompere fino alla prossima label (fora1l:) con disassemble _start,start+20, avremo quasi tutto il codice disassemblato. Ora occorre avviare il programma tramire il comando 'run' tuttavia GDB inizia l'esecuzione e la termina se non vi sono errori. (gdb) run Starting program: /root/source/diciassette.bin Program exited normally. (gdb) Come potete vedere dall'output, nel nostro esempio può essere il caso nostro, tuttavia in progetti è necessario interrompere l'esecuzione al verificarsi di determinate condizioni o altro. impostiamo quindi un punto di break. (gdb) b _start Breakpoint 1 at 0x80481cf: file diciassette.s, line 47. (gdb) run Starting program: /root/source/diciassette.bin Breakpoint 1, _start () at diciassette.s:51 51 forl a1 i $0 $4 Current language: auto; currently asm (gdb) Come potete vedere l'esecuzione dopo il comando 'break' e 'run' si è fermata al nostro punto di partenza '_start' da qui in poi si procederà passo passo, oppure impostando un nuovo break point. (gdb) break nexta1 Breakpoint 2 at 0x804823f: file diciassette.s, line 90. (gdb) cont Continuing. Breakpoint 2, nexta1 () at diciassette.s:90 90 xorl %ebx,%ebx (gdb) Interrompiamo nuovamente l'esecuzione alla label indicata 'nexta1:' tuttavia nonpossiamo più impartire il comando run, in quanto gdb riinizierà il processo da zero, quindi dovremo digitare il comando cont (continue). Breakpoint 2, nexta1 () at diciassette.s:90 90 xorl %ebx,%ebx (gdb) x/40x &aimpiegato 0x8049340 <aimpiegato>: 0x2e2e2e2e 0x2e2e2e2e 0x2e2e2e2e 0x2e2e2e2e 0x8049350 <aimpiegato+16>: 0x002e2e2e 0x2a2a2a2a 0x2a2a2a2a 0x2a2a2a2a 0x8049360 <aimpiegato+32>: 0x2a2a2a2a 0x002a2a2a 0x00001234 0x2e2e2e2e 0x8049370 <aimpiegato+48>: 0x2e2e2e2e 0x2e2e2e2e 0x2e2e2e2e 0x002e2e2e 0x8049380 <aimpiegato+64>: 0x2a2a2a2a 0x2a2a2a2a 0x2a2a2a2a 0x2a2a2a2a 0x8049390 <aimpiegato+80>: 0x002a2a2a 0x00001234 0x2e2e2e2e 0x2e2e2e2e 0x80493a0 <aimpiegato+96>: 0x2e2e2e2e 0x2e2e2e2e 0x002e2e2e 0x2a2a2a2a 0x80493b0 <aimpiegato+112>: 0x2a2a2a2a 0x2a2a2a2a 0x2a2a2a2a 0x002a2a2a 0x80493c0 <aimpiegato+128>: 0x00001234 0x2e2e2e2e 0x2e2e2e2e 0x2e2e2e2e 0x80493d0 <aimpiegato+144>: 0x2e2e2e2e 0x002e2e2e 0x2a2a2a2a 0x2a2a2a2a (gdb) Ora tramite il comando x che visualizza una porzione di memoria otteniamo una immagine della nostra memoria. x/40x significa visualizza la memoria 40 long in formato esadecimale (gdb) x/40c &aimpiegato 0x8049340 <aimpiegato>: 46 '.' 0x8049348 <aimpiegato+8>: 0x8049350 <aimpiegato+16>: 0x8049358 <aimpiegato+24>: 0x8049360 <aimpiegato+32>: (gdb) 46 46 46 42 42 '.' '.' '.' '*' '*' 46 46 46 42 42 '.' '.' '.' '*' '*' 46 46 46 42 42 '.' '.' '.' '*' '*' 46 '.' 46 '.' 0 '\0' 42 '*' 42 '*' 46 46 42 42 42 '.' '.' '*' '*' '*' 46 46 42 42 42 '.' '.' '*' '*' '*' 46 46 42 42 42 '.' '.' '*' '*' '*' 46 '.' 42 '*' 42 '*' 0 '\0' Tramite quest'ultimo comando ed il suffisso 'c' 'carattere' otteniamo un immagine della memoria riferica in carattere l'istruzione corretta era x/220c che mostra tutto il nostro array, tuttavia per motivi di spazio ho mostrato solo un visializzazione di testa. E' possibile così poter verificare di persona l'effettiva corretta esecuzione del codice. (gdb) quit The program is running. debian:~/source# Exit anyway? (y or n) y OPS ! quasi dimenticavo questo è il comando per terminare l'esecuzione di gdb, se il programma e' ancora in esecuzione, gdb vi chiederà una conferma. Array Bidimensionali Nella pratica oltre agli array vengono utilizzate le matrici, cioè array a due dimensioni molto comuni nella maggior parte dei programmi, vedi bitmap e via dicendo, ancora in alcuni algoritmi si fa uso di array a tre dimensioni e così via. Precedentemente abbiamo visto: dato un tipo quale .long .int .struct un metodo per calcolare l'indice di un array ad una dimensione, questo non comportava particolare attenzione, in quanto bastava moltiplicare l'indice per l'effettiva grandezza del tipo in questione. La situazione diventa poco più complicata se prendiamo in esame un array a due dimensioni; nel nostro caso riga, colonna oppure le coordinate x, y. Vediamo un esempio : short array[10][10] ; // definisce un array di interi di 10x10 elementi la stessa codifica in assembly (GAS) è questa, considerando che un short è esattamente 2 byte .bss .lcomm array 10 * 10 * 2 Ora abbiamo allocato un spazio contiguo di 400 byte, per indirizzare correttamente gli indirizzi tramite riga e colonna ci serve un ulteriore dato che è rappresentato dal valore massimo della riga (nel nostro caso dieci), quindi : a = array[2][3] ; a = * ( array + (sizeof (short) * (( col * maxrig ) + rig )) ) ; un pochino contorta, rispettivamente 7 2. ma funzionale. dove maxrig sta per 10 e col, rig Row Major Ordering Assegna gli elementi in ordine muovendo attraverso le righe e poi giù per le colonne; Questo metodo è tipico dei linguaggi ad alto livello quali il 'C' 'Pascal' e via dicendo. rappresentazione della memoria 0 1 2 3 4 5 rappresentazione di una matrice rig / col 1 6 7 8 9 10 11 12 13 14 2 3 4 1 0 1 2 3 2 4 5 6 7 3 8 9 10 11 4 12 13 14 Riferendoci all'esempio precedente : int array [4][4] ; a = array[2][3] a = * ( array + (sizeof (short) * (( col * maxrig ) + rig )) ) ; quindi a = * ( array + ( 2 * (( 3 * 4 ) + 2 )) ) viene calcolato il 14° elemento nella 28^a posizione in &array[0][0]. 15 15 memoria dopo Array di long e memoria Di seguito rappresento, con uno schema la distribuzione in memoria di un array di long, 4 byte; relativamente ad un array bidimensionale con i propri valori : long array[3][3] = { { 1, 2, 3, 4}, {10,20,30,40}, {11,12,13,14}, {90,80,70,60} } ; array contenuto a[3][3] a[3][2] a[3][1] a[3][0] a[2][3] a[2][2] a[2][1] a[2][0] = = = = = = = = 60 70 80 90 14 13 12 11 array array array array array array array array + + + + + + + + 60 56 52 48 44 40 36 32 (c*MAXR+r)*size (3*4+3)*4 60 (c*MAXR+r)*size (2*4+3)*4 44 a[1][3] a[1][2] a[1][1] a[1][0] = = = = 40 30 20 10 array array array array + + + + 28 24 20 16 (c*MAXR+r)*size (1*4+3)*4 28 a[0][3] a[0][2] a[0][1] a[0][0] = = = = 4 3 2 1 array array array array + + + + 12 8 4 0 (c*MAXR+r)*size (0*4+3)*4 12 locazioni formula risultato Nel C come in assembly la codifica inizia sempre da zero quindi una array da 0..3 è composto da 4 elementi; come pui vedere dalla formula, per il calcolo degli indici. Array Multidimensionali La situazione si complica un poco, quando dobbiamo calcolare l'indice di un array multidimensionale a 'n' dimensioni. Solitamente ci riferiamo all'esempio sopra descritto. 2 dim --> indice = 3 dim --> indice = 4 dim --> indice = base + base + base + size * ( y * maxx + x ) ; size * (( z * maxy + y ) * maxx + x ) ; size * ((( w * maxz + z ) * maxy + y ) * maxx + x )) ) ; Da questo esempio è possibile estrapolare un seguente algoritmo : dato ( B ) * ( A°M ) + ( A° ) la generazione successiva di b sarà : ( (b+1) * (b)°m + (b) ) In questo modo otteniamo una sequenza corretta di indicizzazione : 2 dim --> indice = 3 dim --> indice = 4 dim --> indice = base + base + base + size * ( 2° * 1°M + 1° size * (( 3° * 2°M + 2° size * ((( 4° * 3°M + 3° ) ; ) * 1°M + 1° ) ; ) * 2°M + 2° ) * 1°M + 1° )) ) ; (ho scritto al volo questo algoritmo e la rispettiva codifica in assembly alla 1:21 di notte di mercoledi 23 agosto '05) sono un po' stanco; non ho testato un granchè dovrebbe essere tutto corretto, tuttavia se qualcosa non è chiaro o con qualche grossolano errore : mi merito una bella tirata di orecchi!) Se prendiamo in esame array molto grandi, tipo le dimensioni di uno schermo a 1600x1200x32 bit di colore, calcolare ogni indice diventa molto dispendioso,soprattutto se vogliamo inizializzare solamente l'array, un piccolo trucco sta nel riferirsi al valore iniziale dell'array e calcolare la lunghezza finale, e scorrere l'array incremendano di uno .long alla volta (meglio addl $1,%eax molto veloce). Ancora se abbiamo un array a 3 dimensioni del tipo array[5][10][10] possiamo ridurre la complessita creando 5 piccoli array e calcolando gli indici a due dimensioni, al fine di velocizzare i calcoli int int int int int array1[10][10] array2[10][10] array3[10][10] array4[10][10] array5[10][10] ; ; ; ; ; Per quanto riguarda gli array di struttura a più dimensioni il concetto non cambia, occorre far attenzione solamente all'effettiva dimensione della struttura in questione : struct s[10][10][10] ; 3 dim --> indice = base + size struct * (( 3° * 2°M + 2° ) * 1°M + 1° ) ; In questo caso la programmazione delle macro ci viene in soccorso, nel ridurre la complessità e gestione del calcolo degli indici. ndx ndx2 ndx3 ... array size indice array size indice1 MAXindice1 indice2 array size indice1 MAXindice1 indice2 MAXindice2 indice3 Funzioni ricorsive Inserisci questo listato : diciotto.s .include "include/syscall.a" .include "include/stdlibc.a" .include "include/basic.a" .data #...................... static k: .long 1 kdec: .long 1 .text .globl _start _start: #..................... main C_PUTS "\n" pushl $4 call ComputeIndex addl $4,%esp C_PUTS "\n" xorl %ebx,%ebx C_EXIT #........................... Commento : Il listato disiotto.s mostra l'utilizzo di una funzione ricorsiva, cioè una funzione che chiama se stessa n volte, importante in questo tipo di operazioni è definire una condizione di uscita, di modo che la funzione ritorni a ritroso sui suoi passi. Non è particolarmente rilevante l'algoritmo utilizzato, in quanto si avvale di due contatori una in crescita 'k' e la in diminuzione 'kdec' che tengono traccia delle parentesi e dei valori prima e dopo il numero di indici. movl 8(%ebp),%eax # condizione di uscita se raggiunto il # i l valore massimo degli indici cmpl k,%eax jle VisMax Per la restante parte la funzione e' stata scritta in modalità standard con riferimento 8(%ebp) al primo argomento. Variabili Statiche Le variabili 'k' e 'kdec' sono delle variabili globali, cioè definite al di fuori della funzione e accessibili a tutto il file, non a tutti i moduli, in quanto ho omesso il suffisso .globl. Dato che vengono utilizzate solamente dalla funzione in questione, possiamo considerarle delle variabili statiche, in quanto ricordano il proprio valore tra una chiamata e l'altra della funzione. debian:~/source# ./diciotto.bin ((((4)*3M+3°)*2M+2°)*1M+1°) debian:~/source# Questo e' l'output del programma. La prossima pagina presenta per intero la funzione ComputeIndex .type ComputeIndex,@function .data outs: .asciz "*%dM+%d°)" outstart: .asciz "(" outend: .asciz ")" outn: .asciz "(%d)" .text ComputeIndex: pushl %ebp movl %esp ,%ebp movl cmpl jle 8(%ebp),%eax k ,%eax VisMax incl incl pushl call addl k kdec $outstart printf $4 ,%esp pushl 8(%ebp) call ComputeIndex addl $4,%esp #......Numero Indici #..... calcola numero parentesi #..... prima e dopo numero #..... visualizza parentesi #..... arriva fino al numero #..... indici matrice decl pushl pushl pushl call addl kdec kdec kdec $outs printf $12,%esp #..... calcola dopo parentesi jmp esci VisMax: pushl pushl call addl esci: k $outn printf $8,%esp #..... visualizza numero indice movl $1,%eax leave ret debian:~/source# ./diciotto.bin ((((4)*3M+3°)*2M+2°)*1M+1°) debian:~/source 1° 2° 3° 4° x y z w 1°M 2°M 3°M 4°M maxx maxy maxz maxw Tutto e' una bugia ! Riprendiamo il discorso del debug e carichiamo un modulo da esaminare tipo il programma precedente : – – – ddd diciotto ora immettiamo un break point alla linea 1 b 1 e annotiamoci l'indirizzo di _start _start = 0x80481fc Avviamo un'altra sessione di gdb o ddd con il file sedici e ripetiamo lo stesso procedimento annotandoci dopo il break l'indirizzo dello start point : -- _start = 0x080481cc Un' attenta osservazione ci indica che i due programmi sono caricati nella stessa area di memoria eppure convivono separatamente ! Quando un programma viene caricato in memoria ogni .section sezione viene caricata in un punto specifico ad iniziale dall'indirizzo 0x0804:8000, dapprima la sezione codice .text poi quella dei dati .data e così via per le altre .bss ... CODE .SECTION .CODE DATA .SECTION .DATA BSS .SECTION .BSS ...SPAZIO LIBERO... ...BREAK ... STACK STACK ARGUMENT ARGOMENTI DEL PROGRAMMI ENVIRONMENT VARIABILI AMBIENTE PROGRAM NAME NOME DEL PROGRAMMA NULL (DWORD) 00 00 00 00 La sezione .data è esplicitamente dichiarata nel programma, quella .bss viene allocata quando il programma è in esecuzione. L'ultimo indirizzo indirizzabile è 0xbfff:ffff. Alla fine del programma ci sono degli zero e quando all'interno del nostro programma uitilizziamo l'istruzione push questa decrementa la posizione indicata da stack. In questa parte della memoria finiscono tutte le variabili dello stack. Quindi una sezione cresce aumentando, ed un'altra cresce diminuendo; la sezione che si trova nel centro viene chiamata BREAK e non vi è possibilità di accesso a meno che non ne facciamo esplicita richiesta al kernel. Qualsiasi indirizzamento al di fuori di questo range provocherà un errore di protezione : “segmentation fault”. Quindi come dicevo prima ogni indirizzo è una bugia! In effetti il programma in esecuzione accede alla cosidetta memoria virtuale. La memoria fisica che intendiamo è la cosidetta RAM del computer (da 16 a 512mb), se ci riferiamo a quest'ultima certamente identifichiamo l'esatta posizione di codice dove il programma è contenuto. Diversamenta se ci riferiamo alla memoria virtuale identifichiamo una regione di memoria che non corrisponde esattamente a quella fisica, tuttavia il kernel gli fa credere al programma che sta utilizzando la memoria fisica! In effetti ogni programma quando viene caricato in memoria pensa di trovarsi alla posizione 0x0804:4800 e che il suo stack parta da 0x4fff:ffff, il programma tradito fa riferimento a queste locazioni come se effettivamente fossero fisiche; in realtà sono degli indirizzi virtuali. Il processo che assegna un indirizzo virtuale ad un indirizzo fisico è chiamato MAPPING. Precedentemente ho accennato allo spazio libero BREAK, ma non vi ho detto perchè è li. Il motivo è che questa parte di memoria virtuale non è ancora stata MAPPATA in un indirizzo fisico. Il processo di mappatura richiede tempo e spazio, così se vengono mappati tutti gli indirizzi virtuali dei vari programmi in esecuzione, probabilemente non resterà più memoria fisica per eseguire un solo programma! è per questo che viene mantenuta un'area non mappata. La memoria virtuale può essere mappata con dimensioni maggiori che non la memoria fisica, se ho una quantità di memoria ram di 256 mb e il mio programma ne richiede una maggiore quantità ecco che la memoria virtuale viene mappata nel disco , per ottenere più memoria. Chiaramente l'accesso al disco è motlo più lento che non la memoria fisica. La memoria virtuale viene mappata nella partizione di swap, nei file di swap, nei file di paging.Questo metodo estende la quantità di memoria fisica. Linux mappa una parte el disco (swap) come memoria virtuale, quando necessità di questa memoria trasferisce il contenuto direttamente nella memoria fisica; quindi muove un'altra parte della memoria fisica nel disco. La memoria è separata in gruppi chiamate pagine. Una pagina in un x86 è esattamente 4096 byte. Quindi linux indirizza in un colpo 4096 byte da memoria a disco e viceversa, quindi se dobbiamo accedere a soli qualche byte nella memoria virtuale, linux carica l'intera pagina in memoria. Se questa operazione avviene di frequente e molti programmi accedono di frequente a disco/memoria e viceversa ben presto il sistema rallenterà al passo di una lumaca! Anche se con opportuni tecniche e utilizzo di cache, questo può essere notevolmente migliorato. MALLOC / CALLOC / REALLOC / FREE Riferendoci al linguaggio C, abbiamo già pronte 4 simpatiche routine per gestire la memoria. Questa è solo una presentazione di comodo, quello che voglio evitare è quello di insegnarvi il C. Utilizzando le funzioni della libreria standard, pratiche ed efficienti non farete altro che passare parametri e ricevere risultati, lo scopo di questo libro è guidarvi passo passo verso la programmazione in assmbler e non i copia e incolla dei inguaggi attuali. Trovere in un prossimo capitolo un gestore di memoria scritto in assembly. #include <stdlib.h> void void void void *calloc(size_t nmemb, size_t size); *malloc(size_t size); free(void *ptr); *realloc(void *ptr, size_t size); Questi sono i prototipi delle rispettive funzioni della libreria in C, le prime de servo per allocare la memoria, e un breve esempio sul loro utilizzo : int *pointer ; // un puntatore ad interi pointer = calloc ( 100 , sizeof(int) ) pointer = malloc ( 400 ) ; ; // int[100] ; // int[100] ; int = 4 byte if (pointer==NULL) { error !!! } realloc ( pointer , 800 ) ; free ( pointer ) ; ; ; // int[200] // delete Il valore di ritorno è un puntatore con l'ìndirizzo della prima locazione dell'array allocato. Se questo è NULL, significa che non è stato possibile allocare spazio in memoria. Osservate le prossime due dichiarazioni, benchè utilizzano entrambe 4 byte, il C si riferisce alla variabile in memoria 'pint' come ad un puntatore a interi. L'assembly si riferisce alla variabile in memoria come ad un indirizzo a 32 bit. Così nell'esempio degli array di caratteri di '1' byte non viene allocato 1 byte per il puntatore , ma il C si riferisce a *pchar (char) solo per calcolare gli indici, pchar è effettivamente memorizzato come un .long. int *pint ; char *pchar ; pint: .long 0 pchar: .long 0 # char pchar[400] movl pushl call addl movl $0400,%eax %eax malloc $4,%esp %eax,(pchar) # free ( pchar ) movl (pchar),%eax pushl %eax call free addl $4,%esp # char pchar[400] movl $0400,%eax pushl $1 # sizeof pushl %eax call calloc addl $8,%esp Guida di sopravvivenza a GDB Di seguiti presento alcuni comandi utilizzati in gdb, come sempre e per ogni cosa in linux riferirsi al manuale (man gdb) ed alla guida in linea di gdb (gdb help). Per maggiori chiarimenti. Tra parentesi viene indicato il comando abbrevviato. Istruzioni di esecuzione – – – – run (r) stepi (s) continue (c) finish : : : : avvia il programma (ctrl-c) ne interrompe l'esecuzione esegue un passo alla volta continua l'esecuzione fino al prossimo break o exit continua fino alla fine della procedura breakpoint – – – – break line : marca un alinea con un breakpoint break *address : break *_start marca un indirizzo con un break point info break : visualizza elenco breakpoint clear break : cancella break pointer istruzioni per codice – – list inizio,fine : lista il code da linea a linea disassemble : disassembla un parte di programma Visualizzare i registri – – – – info register print/x $eax p/x $st0 p/d $eax x c d f : visualizza elenco di tutti i registri : visualizza il registro %eax : visualizza il regitro ST0 : visualizza %eax in modo decimale modo modo modo modo di di di di visualizzazione visualizzazione visualizzazione visualizzazione esadecimale carattere decimale float variabili – – p/f (var) p/x (var) : : visualizza una variabile double visualizza una variabile in esadecimale memoria – x/(n)x &address : visualizza 'n' caratteri in esadicimale Passaggio di parametri dal 'C' all'assembler e viceversa Questo è un semplice programma in C che richiede l'ausilio di una funzione esterna, 'ComputerIndex' vista in precedenza. si avvale di una variabile 'k' che rappresenterà l'indice e riceverà in un'altra var. 'back' quella di ritorno. Nel programma precedente diciotto.s andranno eseguite le eguenti modifiche : inserire nell'header della funzione la direttiva : .globl ComputerIndex. salvare in un file solo la funzione escludendo la parte di _start. (ComputeIndex.s) – compilare con i seguenti passi : – – - as ComputeIndex.s -o ComputeIndex.o gcc -c prova.c -o prova.o gcc -lc computeindex.o prova.o -o main ./main file : prova.c extern int ComputeIndex( int k ) ; int main ( void ) { long k = 4 ; int back = 0; back = ComputeIndex ( k ) ; } return back ; Ricordo l'opzione -c che genera solo il file oggetto e di linkare il tutto utilizzando gcc. CAPITOLO 11 Programmiamo l' x87 Programmiamo l' x87 Basta parlare per un pò dell'x86 parliamo del suo vicino che è in grado di ampliare le possibilità matematiche offerte dal'x86. In effetti il primo si occupa della gestione dei registri, questi anche a 32 bit sono comunque limitati, nella gestione di grosse cifre numeriche a patto che non vengano scritti algoritmi appositi; nondimeno diventa difficile la gestione dei numeri in virgola mobile. A questo ci pensa il vicino x87 che estende le funzionalià aritmetiche non sono dei numeri in virgola mobile ma anche quella degli interi. STx Il coprocessore matematico ha otto registri interni a 80 bit. Quindi Tutte i numeri letti siano .float .single . double .tfloat verranno sempre memorizzate in 80 bit i nomi dei registri prendono il nome da ST con un numero crescente da 0 a 7 (st0 st1 st2 ... st7 ). Ogni Numero caricato viene caricato in cima allo stack (ST0) i numeri esistenti sono spinti verso il basso della cima fino a (ST7). nella programmazione tramite GAS ci riferiamo ai registri in questo modo : – – %st %st(x) per il registro st0 per gli altri registri da 1 a 7 p.s. Anche se utilizziamo dal gas le direttive .float .double. Tfloat, il valore caricato nei registri sarà sempre un valore a 80 bit. Formati Interi Nel capitolo relativo ai sistemi numerici abbiamo introdotto, alcuni tipi di formato, questi erano relativi alla capacità di memorizzazione dei registri in questione, quindi 8, 16 e 32 bit con le rispettive tabelle. Questi numeri non solo erano limitati in base alla dimensione in byte, ma occorreva suddividere il numero in due parti (complemento a due) per ottenere la restante parte negativa, dimezzando così il valore massimo raggiungibile. Tabella interi x86 Valori senza segno segno bit byte Min Max Valori Min con Max -------------------------------------------------------------------------- 8 1 .byte 16 2 .word 32 4 .long 0 255 -128 0 65.535 -32.768 0 4.294.967.296 -2.147.483.648 +127 +32.765 -2.147.483.647 Tramite l'x87 possiamo gestire interi di più grandi dimensioni. Occorre sottolineare una differenza molto importante, l'x87 gestisce i numeri con segno e utilizza il bit più significativo come il bit del segno , in un intero a 32 bit (0 – 31 ) il bit 31 viene occupato da 1 o 0 per indicare la rpesenza del segno + o -. tabella interi x87 valori con segno bit byte min max ------------------------------------------------------32 4 -2x10^9 +9*10^9 .short 64 8 -9x10^18 +9*10^18 .long l'x87 introduce anche un un ulteriore formato per i numeri BCD Numeri Decimali Codificati in Binario, Questi numeri sono lunghi 10 byte, la massima cifra utilizzabile è il 9, come i precedentei il bit più a sinistra è utilizzato come il bit di segno. valori BCD bit byte min max --------------------------------------------------------80 8 -9x10^18 +9*10^18 (come gli interi ma con formato esteso) I numer BCD vengono normalmente utilizzati nelle trnasazioni finaziarie, l'importanza di questo formato in base 10 è dato dal fatto che permette di eliminare gli errori di arrotondamento dovuto alleconversioni Formati Reali Come per i numeri interi l'x87 amplia la capacità dell'elaboratore aggiungendo 2 formati di numeri reali, quelli corti a 4 byte e quelli lunghi a 64. tabella Valori Reali bit byte min max 32 64 4 8 8.43x10^-37 3.7x19^38 3.4 x10^4392 1.2x10^4392 .word .quad I numeri reali sono memorizzati in forma “normalizzata” ciò significa che in questo formato viene scritto il numero 1 seguito dalla mantissa in questione con il rispettivo esponente. bit 31 Segno bit 30 – 23 Esponente bit 22 – 0 Mantissa Le cose non sono poi così semplici, in quanto dato che nel formato normalizzato il primo numero è sempre 1 si è deciso di rendere implicito questo numero e memorizzare solo la mantissa. A complicare il tutto gli esponenti vengono memorizzati con un valore detto di compensazione. valore offset reale corto 4 byte reale lungo 8 byte +127 +1023 aggiunto all'esponente aggiunto all'esponente Vediamo ora dopo una noiosa parte teorica un pò di trattazione pratica. SUFFISSI Quando carichiamo un numero nel coprocessore matematico, dobbiamo specificare la sua grandezza,all'assemblatore; al fine di gestire correttamente le operazioni di lettura e scrittura in memoria. Questi suffissi vanno applicati alle istruzioni per specificare la quantità di dati che l'istruzione stessa dovrà indirizzare. SUFFISSI per i numeri reali NASM TIPO BIT SUFFISSO DD .FLOAT / .SINGLE 32 S DQ .DOUBLE 64 L DT .TFLOAT 80 T SUFFISSI per i numeri interi NASM TIPO BIT SUFFISSO DB .BYTE 8 B DW .WORD 16 W DD .INT / .LONG 32 L DQ .QUAD 64 Q .OCTA 128 Nel listato successivo, vengono presentati 3 tipologie di numeri singola, doppia e precisione estesa, e un esempio molto semplice di come caricare i numeri e gestirli, va ricordato di caricare un numero come single (32 bit) trattato come .double (L) in questo caso il suffisso (L) come long potrebbe fuorviare in quanto .long = 4 e .quad = 8, ecco perchè fare attenzione onde evitare insidiosi bugs. Ancora ricordo che la .quad, .double sono lunghi 8 byte, quindi occore passare la parte bassa, poi quella alta alla printf. L'utilizzo dei suffissi è molto importante nelle operazioni di lettura/memorizzazione per assicurarsi di gestire il dato corretto, altre operazioni tipo quelle aritmetiche, non utilizzano suffissi (ex fmul) in quanto gestisco internamente il numero a 80 bit, anche gli interi vengono convertiti in numero a precisione estesa a 80 bit. suffisso P Quando in una istruzione compare anche il suffisso p, significa POP che viene eseguita l'istruzione e viene estratto dalla cima il registro. – – fstl dest fstpldest --> --> memorizza st0 in dest (.double) memorizza st0 in dest e POP (.double) Esempio utilizzo suffissi : .include "include/syscall.a" .include "include/stdlibc.a" .include "include/basic.a" .data out: .asciz "\n %g \n " risultato: .double 1.5 op1: .float 1.2 op2: .double 1.3 op3: .tfloat 1.4 .text _start: .globl _start finit flds fstl op1 risultato pushl risultato+4 pushl risultato pushl $out call printf addl $12,%esp xorl %ebx,%ebx C_EXIT L'istruzione FINIT serve per inizializzare l'x87 e svuotare lo stack tutti i reigistri sono marcati come inutilizzati, tutte le eccezioni sono mascherate, ed il controllo di arrotondamento è impostato al più vicino, con modalità di funzionamento in doppia precisione. L'antagonista FNINT, non controlla che vi siano eccezioni mascherate, se vi sono esegue comunque l'inizializzazione. Caricare dalla memoria Una piccola nota tutte le istruzioni iniziano sempre con 'F' INTERI REALI FILD FLD BCD FBLD Copia in memoria i numeri INTERI REALI FIST FST BCD FBST Copia in memoria i numeri e scarica dallo stack x87 INTERI REALI FISTP FSTP BCD FBSTP Operazioni aritmetiche INTERI REALI BCD FIADD FADD FADD FISUB FSUB FSUB FIDIV FDIV FDIV FIMUL FMUL FMUL Esempi : – – – – – – – fadd op1 faddr op1 --> --> st0 st0 := st0 – op1 := op1 – st0 fsub fsubr fsub fsubr --> --> --> --> st0 st0 dest st0 := := := := – – – fmul op1 fmul dest,st0 --> fimul dest,st0 --> --> st0 := st0 * op1 dest := dest * st0 st0 := st0 * dest (integer) – – – fidiv op1 fidivr op1 fdiv op1 --> --> --> op1 op1 dest,st0 dest,st0 st0 – op1 op1 – st0 dest – st0 st0 - dest st0 st0 st0 := st0 / op1 := op1 / st0 (integer) := st0 / op1 alcuni esempi .include include/syscall.a" .include "include/syscall.a" .include "include/stdlibc.a" .include "include/stdlibc.a" .include "include/basic.a" .include "include/basic.a" .data .data out: out: .asciz "\n %d \n " .asciz "\n %g \n " risultato: risultato: .long 0 .double 0.0 op1: op1: .word 3 .double 2.0 op2: op2: .word 6 .double 3.0 .text .text .globl _start .globl _start _start: _start: fild op1 fldl op1 fimul op2 fmull op2 fwait fistp risultato fstl risultato pushl risultato+4 pushl risultato pushl risultato pushl $out pushl $out call printf addl $8,%esp call printf addl $8,%esp xorl %ebx,%ebx xorl %ebx,%ebx C_EXIT C_EXIT Operazione di confronto .include "include/syscall.a" .include "include/stdlibc.a" .data op1: .float 1.2 op2: .double 1.3 op3: .tfloat 1.4 .text _start: .globl _start finit flds fcomp fstsw sahf ja op1 op2 %ax # 1.2 # 1.3 maggiore # if x above y op1 > op2 minore: C_PUTS "\n op1 < op2" jmp fine maggiore: C_PUTS "\n op1 >= op2" fine: xorl %ebx,%ebx C_EXIT Commento : Di particolare rilevanza è la parte del listato dopo _start, che si occupa di caricare in memoria un valore .float in op1 e di confrontarlo con il valore .double op2, il risultato viene messo nella “word” di stato del coprocesssore, quindi è possibile memorizzarla (fstsw) come di consueto nel registro o in una posizione in memoria, e così tramire sahf settare i vari flag, cosi possiamo effettivamente controllarlne il valore con le usuali operazione dell'x86. Anche l'istruzione di confronto si avvale di diversi suffissi : – – – fcom fcomp ficom op1 op1 op1 --> --> --> if st0 ? op1 if st0 ? op1 then POP st0 come la prima ma per i numeri INTEGER – queste istruzioni NON modificano i flag ma occorre avvalersi della procedura esposta precedentemente per poter valutare correttamente i flag. La parola di stato Quando viene eseguita un'operazione di confronto FCOM codifica la parola di stato nel seguente modo (avvalendosi di 4 bit interni ) c3 c2 c1 c0 descrizione 0 0 1 1 0 0 0 1 ? ? ? ? 0 1 ? 1 st st st st > operando < operando = operando non confrontabile La parola di stato può essere prelevata attraverso FSTSW ed il contenuto della WORD può essere trasferito in memoria e analizzato in base ai bit c3-c0. Modificare direttamente i flag Dal pentium pro in poi sono state introdotte due istruzione che consentono di modificare direttamente il contenuto dei flag senza dover ricorrere a questo procedimento : finit flds op1 fldl op2 fcomi %st(1) # 1.2 # 1.3 # if %st ? st(1) ja # if x above y maggiore Come vedete da questo esempio viene eseguito un confronto tra op1 e op2 modificando i flag di stato dell'x86, unica accortezza che questa istruzione funziona solo con i registri st(x) come vedete abbiamo caricato il valore in un secondo registro. Non confondete questa istruzione fcomi con ficom che confronta due interi e non modifica i flag. FNSTSW Memorizza la word di stato senza attesa. Questa istruzione a differenza della sua omonima non attende che il bit 15 (busy bit) sia libero, ma memorizza direttamente la word. FCOMPP Ancora un istruzione di comparazione, ma come indicato nei suffissi, estrae due valori dallo stack. Confronta st con st(1) e poi esegue pop st e pop st(1) FLDCW Carica la parola di stato da una posizione o da un registro, a 16 bit (word) Registro Parola di stato Il registro può essere cosi schematizzato : 15 14 B B C3 -> C3-C0 -> 13 12 TOP 11 10 C2 9 8 C1 C0 7 ES 6 SF 5 PE 4 3 2 1 UE OE ZE DE 0 IE imegnato (busy) questo bit è a 1 nei seguenti casi : – x87 sta eseguendo un'istruzione ; – viene indicata un'eccezione non mascherata ; Codici di condizione, questi flag vengono impostati quando viene eseguita un'operazione di confronto, verifica o esame in virgola mobile ; TOP -> Cima dello stack, qundo viene inserito un valore nello stack il reg TOP viene decrementato di 1 (3 bit) ; ES -> Riepilogo degli errori. impostato a 1 ogni volta che l'x87 genera un eccezione nn mascherata. ( bit 0 – 5 ) ; SF -> Errore di Stack (stack fault). Se vengono immessi troppi operandi (overflow) viene generato un errore. I bit da 0 a 5 sono Bit di Eccezzione, vengono impostati a 1 ogni qualvolta si presenta una determinata condizione di errore; Da sottolineare il fatto, che una volta settati, questi bit rimangono tali, fino a quanto il programmatore non carica nel riegistro di stato un nuovo valore. PE Eccezione di precisione : questo Bit viene settato quandol'x87 non è in grado di rappresentare correttamente un numero in virgola mobile. UE Eccezione di underflow : quando il risultato di un operando è troppo piccolo per essere rappresentato . OE Eccezione overflow : quando il risultato di un'operazione in virgola mobile è troppo grande per essere rappresentato. ZE Eccezione Divisione per zero : settato a 1 ogni volta che viene tentata una divisione per zero. DE Eccezione non-normale : quando incontra un numero non-normale ! nel formato non normalizzato IE Eccezione Operazione non valida : tutte le condizioni di errore non presentate nelle eccezioni precedenti. (tipo errori matematici = sqrt(-1)!) Registro della parola di Controllo Questo è un registro molto importante in quanto consente di modificare il comportamento del processore matematico. Il registro di controllo può essere cosi schematizzato : 15 * 14 * 13 12 * C2 11 10 9 RC 8 PC 7 * 6 * 5 4 3 PM UM OM 2 ZM 1 DM 0 IM bit 12 (controllo di infinito) RC Controllo di arrotondamento : Questo campo sepcifica, come trattare i valori che non possono essere rappresentati correttamente. Impostatzioni di RC : 00 01 10 11 – – – – -> -> -> -> PC arrotondamento arrotondamento arrotondamento arrotondamento al prossimo più vicino all'infinito negativo all'infinito positivo allo zero. Controllo di precisione : Indica quale formato di precisione utilizzare in corrispondenza di un'operazione aritmentica (add,sub,mul,div,sqrt) : – – – – 00 01 10 11 -> -> -> -> precisione singola 32 bit ; riservato ; precisione doppia 64 bit ; precisione estesa 80 bit ; - PM UM OM ZM IM -> -> -> -> -> maschera maschera maschera maschera maschera di precisione ; di underflow ; di overflow ; di divisione per zero ; per operazioni non valide FSTCW / FNSTCW Istruzioni rispettivamente Floating Point Store Control Word ( No wait state). Esempio FSTCW %ax FLDCW Istruzione che carica La parola di controllo di stato, da un registro o da una locazione di memoria, esempio FLDCW %ax. Registro di etichetta E' un registro a 16 bit, costituito da 8 campi da 2 bit ciascuno. Corrispondente a ciascun registro in virgola mobile da st0 a st7 : valore della coppia di bit : – – – – 00 -> 01 -> 10 -> 11 -> il numero in virgola mobile è corretto ; il registro contiene 0.0 il regitro contiene : valore infinito / valore non valido ; rgitro vuot non utilizzato ; Il programma sotto riportato esegue un controllo, per verificare se c'è stata una divisione per zero. Riassumendo quanto esposto in precedenza. Traminte FINIT inizializzo lo stato dell'x87 e tramite l'istruzione FCLEX , riporto a zero eventuali errori precedenti, poi carico un valore .float e lo divido per 0. Memorizzo la parola di stato in %ax , ne sposto il contenuto nei flag, e testo il bit 2 di %ah con 1 per vedere se il flag Zero è impostato a 1. Se ZF = 1 significa che divisore era zero. (fnclex azzerato i flag di errore senza nessuna attesa) .include "include/syscall.a" .include "include/stdlibc.a" .data op1: .float 1211.2 op2: .double 1.3 op3: .double 0.0 .text _start: .globl _start finit fclex flds fdiv fstsw sahf test je op1 op3 %ax # 1.2 # 0.0 $2,%ah divzero divnonzero: C_PUTS "\n Divisione non Zero" jmp fine divzero: C_PUTS "\n Divizione zero" fine: xorl %ebx,%ebx C_EXIT Ancora sulle istruzioni di controllo FTST Controlla se st = 0.0 C3 C0 Significato 0 0 1 1 0 1 0 1 ST > 0 ST < 0 ST = +0 / -0 ST non compatibile. FXAM Modifica i flag di condizione in base al valore contenuto C3 C2 c1 c0 significato 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 0 0 0 0 1 1 1 1 0 0 0 0 1 1 1 1 0 0 1 1 0 1 1 1 0 0 1 0 0 0 1 1 0 1 0 1 0 1 0 1 0 0 1 0 0 1 0 1 formato non normalizzato NAN formato non normalizzato formato non normalizzato Formato normalizzato infinito + normale infinito +0 vuoto -0 vuoto +De-Normale vuoto -De-Normale vuoto FPREM FPREM calcola st mod ( ST(1) ); in ST viene memorizzato il resto della divisione imposta c3 c1 c0 con i bit meno significativi del quoziente generato: c3 c2 c1 c0 significato ? 0 0 0 0 1 1 1 1 1 0 0 0 0 0 0 0 0 ? 0 0 1 1 0 0 1 1 ? 0 1 0 1 0 1 0 1 calcolo imcompleto quoziende mod 8 = 0 quoziente mod 8 = 1 quoziente mod 8 = 4 quoziente mod 8 = 5 quoziente mod 8 = 2 quoziente mod 8 = 6 quoziente mod 8 = 3 quoziente mod 8 = 7 Miscellanea FABS FCSH FXCH fxstract fnop Valore assoluto st = abs(st) Cambio segno st = -st scambia i registri st(x) swap st estrae esponente ST e mantissa ST1 non fa nulla, simile a nop dell'x86 matematiche fpatan fptan fsqrt fyl2x fyl2x1 f2xm1 fcos fsin fscale fsincos --> --> --> --> --> --> --> --> --> --> arco tangente st := arctan ( st(1) / st ) ; tangente st := tan ( st ) ; radice quadrata st := sqrt ( st ) ; log2 X st := st(1) * log2(st) log2 X st := st(1) * log2(st + 1) 2x -1 st := 2^st * -1 coseno st := cos ( st ) ; seno st := sin ( st ) st := st * 2 ^ int( st(1) ) temp = st st = sin (temp) # st0 push = cos(temp) # st1 Costanti FLD1 FLDZ FLDL2E FLDL2T FLDLG2 FLDLN2 FLDPI memorizza memorizza memorizza memorizza memorizza memorizza memorizza 1 in cima allo stack 0 in cima allo stack logaritmo base 2 (e) logaritmo base 10 (2) logaritmo base 2 (10) logaritmo naturale (2) il pi greco (3.14.159...) st0 st0 MANIPOLAZIONE STACK FDECSTP Decrementa lo stack e fa posto per un valore; --TOP & 0x07 Esempio prima dopo ---------st 1.2 st1 3.2 st ? st1 1.2 st2 3.2 FINCSTP Incrementa lo stack pointer; ++POP & 0x07 prima st st1 dopo 1.2 2.2 st7 st 1.2 2.2 Distanza Fra 2 punti Dato un piano cartesiano con coordinate x y non obbliquo, è possibile ricavare la distanza tra due punti con la seguente formula : dist = sqrt ( (x2-x1)^2 + (y2-y1)^2 ) ; la traduzione nel linguaggio assembly è la seguente : .include "include/syscall.a" .include "include/stdlibc.a" .data out: .asciz "\n %g \n " dist: .double 1.5 # distanza x1: .double 2.2 y1: .double 2.3 # coordinata x primo punto x2: .double 9.2 y2: .double 9.3 # coordinata x secondo punto # coordinata y primo punto # coordinata y secondo punto .text _start: .globl _start finit fclex fldl fsubl fmul fldl fsubl fmul # reinizializza il coprocessore matematico # reimposta gli errori a 'nessun errore' x2 x1 %st(0) y2 y1 %st(0) # carica x 2° punto # sottrai x 1° punto # elevalo alla seconda # carica y 1° punto # sottrai y 1° punto # elevalo alla seconda st0 st0 st0 st0 st0 st1= st0 precedente st0 faddp %st(1) # addiziona tra loro i 2 numero ed estrai dall stack fsqrt # calcola la radice quadrata e estrai dallo stack fwait # aspetta che l'operazione sia conclusa # meglio fwait che wait dell'x86 # memorizza il double nella vr distanza fstpl dist xorl %ebx,%ebx C_EXIT CAPITOLO 12 Iniziamo con l'assembly ! Iniziamo con l'assembly ! Ma come mi dite voi? fino adesso cosa abbiamo fatto ? Finora abbiamo utilizzato delle funzioni della libreria 'C' per semplificare la gestione del codice. Questo equivale a passar degli argomenti a delle funzioni e non a programmarle. Non è che a basso livello poi ci si comporti differentemente, in quanto con alcune chiamate di sistema inizializzo dei registri per adattarli agli scopi prefissati magari scrivo direttamente in determinate locazioni di memoria per raggiungere lo scopo, quel che conta è che il codice è 'raw' cioè grezzo e può far a meno dell'utilizzo di chiamate esterne. Certo è più difficile (difficile = è quello che non si conosce e a cui manca la pratica), ma in questo modo si capisce come esattamente le cose funzionano, senza esssere confinati nel limbo del copia e incolla! Iniziamo con HELLO WORLD ! listato : ventuno.s .section .data ###################################################### WRITE # ###################################################### .macro SC_WRITE buffer len=1 .endm movl xorl movl movl $4 %ebx $\buffer $\len int $0x80 , %eax , %ebx , %ecx , %edx # write = 4 # stdin = 0 # car = 1 ##################################################### .data outs: .asciz "\nHello World!\n" .equ len_outs , . - outs # . rappresenta l'indirizzo attuale ! .text .globl _start _start: SC_WRITE outs len_outs xorl %ebx,%ebx SC_EXIT N.B. : nel file notate l'utilizzo di un singolo punto '.' a livello della direttiva .equ. Il punto '.' sta ad indicare l'indirizzo di memoria corrente. Quindi sottraendo da questo l'indirizzo della stringa ottengo la lunghezza. Commento : L'output, di questo programma non è dissimile da quello presentato nel programma : primo.s, visualizza su schermo la scritta “Hello world!”. La differenza è notevole in quanto viene disturbato direttamente il kernel, tramite una chiamata di sistema. Le chiamate di sistema si avvalgono dell'ausilio di alcuni registri per espletare le proprie funzioni. – %eax contiente il numero della chiamata di sistema ; Poi a seconda della chiamata in questione, i relativi registri vengono caricati con i dati richiesti. Nel nostro caso : – – – – %eax : contiente il numero della chiamata di sistema ; %ebx : contiene lo standard output (stdout = 0) ; %ecx : contiene l'indirizzo della locazione da visualizzare ; %edx : contiene il numero dei caratteri da visualizzare ; tutte le chiamate di sitema fanno riferimento al numero esadecimale 0x80. La normale esecuzione del programma viene interrotta e passata al kernel che accede all'indirizzo della syscall chiamata tramite un tabella di indirizzi. I numeri relativi alle “system call” sono contenute nel file unist.h esempio nella mia architettura : /usr/src/kernel-source-2.6.8/include/asm-i386/unistd.h In questo file sono memorizzati i numeri delle chiamate,di seguito riporto per esteso la parte iniziale del file, a cui potete riferivi come tabella. Potete anche riferirvi al file syscall.h. esempio : #define __NR_restart_syscall #define __NR_exit 1 #define __NR_fork 2 0 Non includete nelle vostre chiamate c questa linea è un errore riferitivi all'include con : include <sys/syscall.h>. /usr/include/bits/syscall.h Qui vengono riportate ancora i numeri delle syscall, ma abbinate ad un nome identificativo : esempio : /* Generated at libc build time from kernel syscall list. */ #ifndef _SYSCALL_H # error "Never use <bits/syscall.h> directly; include <sys/syscall.h> instead." #endif #define SYS__llseek #define SYS__newselect #define SYS__sysctl #define SYS_access __NR__llseek __NR__newselect __NR__sysctl __NR_access #ifndef _ASM_I386_UNISTD_H_ #define _ASM_I386_UNISTD_H_ /* * This file contains the system call numbers. */ #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define __NR_restart_syscall 0 __NR_exit 1 __NR_fork __NR_read __NR_write __NR_open __NR_close __NR_waitpid __NR_creat __NR_link 9 __NR_unlink __NR_execve __NR_chdir __NR_time __NR_mknod __NR_chmod __NR_lchown __NR_break __NR_oldstat __NR_lseek __NR_getpid __NR_mount __NR_umount __NR_setuid __NR_getuid __NR_stime __NR_ptrace __NR_alarm __NR_oldfstat __NR_pause __NR_utime __NR_stty 31 __NR_gtty 32 __NR_access __NR_nice __NR_ftime __NR_sync __NR_kill __NR_rename __NR_mkdir __NR_rmdir __NR_dup 41 __NR_pipe __NR_times __NR_prof __NR_brk __NR_setgid __NR_getgid __NR_signal __NR_geteuid __NR_getegid __NR_acct 51 __NR_umount2 __NR_lock __NR_ioctl __NR_fcntl __NR_mpx 2 3 4 5 6 7 8 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 33 34 35 36 37 38 39 40 42 43 44 45 46 47 48 49 50 52 53 54 55 56 #define __NR_setpgid 57 #define __NR_ulimit 58 #define __NR_oldolduname 59 #define __NR_umask 60 #define __NR_chroot 61 #define __NR_ustat 62 #define __NR_dup2 63 #define __NR_getppid 64 #define __NR_getpgrp 65 #define __NR_setsid 66 #define __NR_sigaction 67 #define __NR_sgetmask 68 #define __NR_ssetmask 69 #define __NR_setreuid 70 #define __NR_setregid 71 #define __NR_sigsuspend 72 #define __NR_sigpending 73 #define __NR_sethostname 74 #define __NR_setrlimit 75 #define __NR_getrlimit 76 /* Back compatible 2Gig limited rlimit */ #define __NR_getrusage 77 #define __NR_gettimeofday 78 #define __NR_settimeofday 79 #define __NR_getgroups 80 #define __NR_setgroups 81 #define __NR_select 82 #define __NR_symlink 83 #define __NR_oldlstat 84 #define __NR_readlink 85 #define __NR_uselib 86 #define __NR_swapon 87 #define __NR_reboot 88 #define __NR_readdir 89 #define __NR_mmap 90 #define __NR_munmap 91 #define __NR_truncate 92 #define __NR_ftruncate 93 #define __NR_fchmod 94 #define __NR_fchown 95 #define __NR_getpriority 96 #define __NR_setpriority 97 #define __NR_profil 98 #define __NR_statfs 99 #define __NR_fstatfs 100 #define __NR_ioperm 101 #define __NR_socketcall 102 #define __NR_syslog 103 #define __NR_setitimer 104 #define __NR_getitimer 105 #define __NR_stat 106 #define __NR_lstat 107 #define __NR_fstat 108 #define __NR_olduname 109 #define __NR_iopl 110 #define __NR_vhangup 111 #define __NR_idle 112 #define __NR_vm86old 113 #define __NR_wait4 114 #define __NR_swapoff 115 #define __NR_sysinfo 116 #define __NR_ipc 117 #define __NR_fsync 118 #define __NR_sigreturn 119 #define __NR_clone 120 #define __NR_setdomainname 121 #define __NR_uname 122 #define __NR_modify_ldt 123 #define __NR_adjtimex 124 #define __NR_mprotect 125 #define __NR_sigprocmask 126 #define __NR_create_module 127 #define __NR_init_module 128 #define __NR_delete_module 129 #define __NR_get_kernel_syms 130 #define __NR_quotactl 131 #define __NR_getpgid 132 #define __NR_fchdir 133 #define __NR_bdflush 134 #define __NR_sysfs 135 #define __NR_personality 136 #define __NR_afs_syscall 137 /* Syscall for Andrew File System */ #define __NR_setfsuid 138 #define __NR_setfsgid 139 #define __NR__llseek 140 #define __NR_getdents 141 #define __NR__newselect 142 #define __NR_flock 143 #define __NR_msync 144 #define __NR_readv 145 #define __NR_writev 146 #define __NR_getsid 147 #define __NR_fdatasync 148 #define __NR__sysctl 149 #define __NR_mlock 150 #define __NR_munlock 151 #define __NR_mlockall 152 #define __NR_munlockall 153 #define __NR_sched_setparam 154 #define __NR_sched_getparam 155 #define __NR_sched_setscheduler 156 #define __NR_sched_getscheduler 157 #define __NR_sched_yield 158 #define __NR_sched_get_priority_max #define __NR_sched_get_priority_min #define __NR_sched_rr_get_interval 161 #define __NR_nanosleep 162 #define __NR_mremap 163 #define __NR_setresuid 164 #define __NR_getresuid 165 #define __NR_vm86 166 #define __NR_query_module 167 #define __NR_poll 168 159 160 #define __NR_nfsservctl 169 #define __NR_setresgid 170 #define __NR_getresgid 171 #define __NR_prctl 172 #define __NR_rt_sigreturn 173 #define __NR_rt_sigaction 174 #define __NR_rt_sigprocmask 175 #define __NR_rt_sigpending 176 #define __NR_rt_sigtimedwait 177 #define __NR_rt_sigqueueinfo 178 #define __NR_rt_sigsuspend 179 #define __NR_pread64 180 #define __NR_pwrite64 181 #define __NR_chown 182 #define __NR_getcwd 183 #define __NR_capget 184 #define __NR_capset 185 #define __NR_sigaltstack 186 #define __NR_sendfile 187 #define __NR_getpmsg 188 /* some people actually want streams */ #define __NR_putpmsg 189 /* some people actually want streams */ #define __NR_vfork 190 #define __NR_ugetrlimit 191 /* SuS compliant getrlimit */ #define __NR_mmap2 192 #define __NR_truncate64 193 #define __NR_ftruncate64 194 #define __NR_stat64 195 #define __NR_lstat64 196 #define __NR_fstat64 197 #define __NR_lchown32 198 #define __NR_getuid32 199 #define __NR_getgid32 200 #define __NR_geteuid32 201 #define __NR_getegid32 202 #define __NR_setreuid32 203 #define __NR_setregid32 204 #define __NR_getgroups32 205 #define __NR_setgroups32 206 #define __NR_fchown32 207 #define __NR_setresuid32 208 #define __NR_getresuid32 209 #define __NR_setresgid32 210 #define __NR_getresgid32 211 #define __NR_chown32 212 #define __NR_setuid32 213 #define __NR_setgid32 214 #define __NR_setfsuid32 215 #define __NR_setfsgid32 216 #define __NR_pivot_root 217 #define __NR_mincore 218 #define __NR_madvise 219 #define __NR_madvise1 219 /* delete when C lib stub is removed */ #define __NR_getdents64 220 #define __NR_fcntl64 221 /* 223 is unused */ #define __NR_gettid 224 #define __NR_mq_timedreceive (__NR_mq_open+3) #define __NR_mq_notify (__NR_mq_open+4) #define __NR_mq_getsetattr (__NR_mq_open+5) #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define __NR_readahead 225 __NR_setxattr 226 __NR_lsetxattr 227 __NR_fsetxattr 228 __NR_getxattr 229 __NR_lgetxattr 230 __NR_fgetxattr 231 __NR_listxattr 232 __NR_llistxattr 233 __NR_flistxattr 234 __NR_removexattr 235 __NR_lremovexattr 236 __NR_fremovexattr 237 __NR_tkill 238 __NR_sendfile64 __NR_futex __NR_sched_setaffinity __NR_sched_getaffinity __NR_set_thread_area __NR_get_thread_area __NR_io_setup __NR_io_destroy __NR_io_getevents 247 __NR_io_submit __NR_io_cancel __NR_fadvise64 #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define __NR_exit_group 252 __NR_lookup_dcookie 253 __NR_epoll_create 254 __NR_epoll_ctl 255 __NR_epoll_wait 256 __NR_remap_file_pages 257 __NR_set_tid_address 258 __NR_timer_create 259 __NR_timer_settime (__NR_timer_create+1) __NR_timer_gettime (__NR_timer_create+2) __NR_timer_getoverrun (__NR_timer_create+3) __NR_timer_delete (__NR_timer_create+4) __NR_clock_settime (__NR_timer_create+5) __NR_clock_gettime (__NR_timer_create+6) __NR_clock_getres (__NR_timer_create+7) __NR_clock_nanosleep (__NR_timer_create+8) __NR_statfs64 268 __NR_fstatfs64 269 __NR_tgkill 270 __NR_utimes 271 __NR_fadvise64_64 272 __NR_vserver 273 __NR_mbind 274 __NR_get_mempolicy 275 __NR_set_mempolicy 276 __NR_mq_open 277 __NR_mq_unlink (__NR_mq_open+1) __NR_mq_timedsend (__NR_mq_open+2) #define __NR_sys_kexec_load #define NR_syscalls 284 239 240 241 242 243 244 245 246 248 249 250 283 Miglioriamo L'output Cosa ci manca?! beh solitamente l'output che avete visto è una semplice stringa dopo il prompt del cursore, a mio parere è un po' bruttino. Sarebbe più carino cancellare lo schermo e magari piazzare l'output al centro del video e perchè no in maniera colorata ! Per quanto riguarda il DOS , l'output del video inizia dalla locazione $0b800 per i monitor a colori e $0b00 per quelli monocromatici, oggi pezzi da museo. Quindi tramite la manipolazione diretta di queste area di memoria è possibile visualizzare il carattere con gli attributi. Linux lavorando in modalità protetta non consente di scrivere al di fuori di locazioni di memoria non consentite, se tentiamo di scrivere su queste locazioni, il kernel di informerà di un bell errore di “SIGSEGV FAULT”. Ancora Esiste la possibilità in DOS di accedere direttamente alla memoria video tramite il BIOS , mediante l'interrupt che funzionano similmente come le system_call poter scrivere nell'area interessata. (Purtroppo) linux dopo l'avvio si sbarazza del bios (menonale) e gestisce direttamente tutto in linguaggio macchina. allora come fare ? qui abbiamo due soluzioni o scrivere del “device” che andranno caricati nel kernel, procedura complessa oppure ... tramite il terminale ! Quando eseguite l'output in modalità testo, siete all'interno di una console, meglio di un terminale remoto. Il terminale emulato dal linux è il VT102, con alcune estensioni. I terminali vengono controllati inviando loro delle sequenze di ESCAPE; al fine di settare le varie modalità di visualizzazione. Per esempio per calcellare lo schermo : ##################################################### Codici VT102 # ##################################################### vthome: .asciz "\033[01;01H" .equ len_vthome, . - vthome vtclear: .asciz "\033[2J" .equ len_vtclear,4 Come vedete la stringa vtclear, contiene una sequenza di escape : – ESC [ 2 J ( 4 caratteri) Tramite questo comando si informa il termina di cancellare l'attuale output a video per l'intero schermo. Solitamente quando cancelliamo lo schermo ci aspettiamo di trovare il cursore nella posizione dalle coordinate riga,colonna 1,1. Ecco venirvi in soccorso una seconda stringa la vthome che appunto produce il risultato desiderato – ESC [ {xx} ; {yy} H (8 caratteri) {x,y} rappresentano le coordinate dello schemo ( normalmente 80x25 ) \033 è la sequenza ottale del carattere di controllo 27 decimale 0x1B esadecimale a titolo di completezza riporto per esteso un esempio : .include "include/syscall.a" .include "include/vt102.s" .section .data ###################################################### VT (buffer) # ###################################################### .macro vt buffer movl xorl movl movl int $4 ,%eax %ebx ,%ebx $\buffer ,%ecx $len_\buffer,%edx $0x80 # stdin = 0 # lunghezza .endm .section .data vthome: .asciz "\033[01;01H" .equ len_vthome, . - vthome vtclear: .asciz "\033[2J" s2: .asciz "\nHello World!\n" .equ len_s2, . - s2 .text .globl _start _start: vt vthome vt vtclear SC_WRITE s2 , len_s2 xorl %ebx,%ebx SC_EXIT Commento : Per semplificare la gestione delle stringhe e la propria lunghezza ho scritto questa semplice macro, che si prende carico della stringa di output e di ricercare la sua lunghezza semplicenemnte aggiungendo 'len_' alla stringa data che dovrà essere ovviamente predefinita come indicato sopra. Altri codici : per riferivi ai codice digitate : man console_codes ; e avrete ulteriori informazioni relativi a terminali e alle sequenze di escape : Di seguito ne riporto solo alcuni e successivamente ne formisco gli esempi di utilizzo. par result 0 1 2 4 5 7 reset all attributes to their defaults set bold set half-bright (simulated with color on a color display) set underscore (simulated with color on a color display) (the colors used to simulate dim or underline are set using ESC ] ...) set blink set reverse video ... 21 22 24 25 27 set normal intensity (this is not compatible with ECMA- 48) set normal intensity underline off blink off reverse video off 30 31 32 33 34 35 36 37 38 39 set black foreground set red foreground set green foreground set brown foreground set blue foreground set magenta foreground set cyan foreground set white foreground set underscore on, set default foreground color set underscore off, set default foreground color 40 41 42 43 44 45 46 47 49 set black background set red background set green background set brown background set blue background set magenta background set cyan background set white background set default background color Questo set di codici viene utilizzato con la seguente stringa : vtblink_on: .asciz "\033[5m\0" stringa generale : ESC [ {x} m (lunghezza 4) Dove X rappresenta il codice sopra riportato. E' facile intuire il funzionamento di ogni valore. GOTOXY ! Prima avevo utilizzato la sequenza di caratteri che spostava il cursore alla posizione 1,1 tuttavia si trattava di una sequenza fissa, è molto più utile passare dei parametri relativi alla rig. & col. . ############################################# gotoxy ! # ############################################# Locate x y ! .data _vtrc: #.......1....34.67. .asciz "\033[01;01H" .equ len_vtrc , . - _vtrc .macro vtrc _riga , _col .data 0: .ascii "\_riga" .byte '*' 1: .ascii "\_col" .byte '*' .text #.......................Riga .equ len_vtrc , . - _vtrc .macro vtrc _riga , _col .data 0: .ascii "\_riga" .byte '*' 1: .ascii "\_col" .byte '*' .text #.......................Riga movb (0b+0) , %al movb (0b+1) , %ah movw %ax , (_vtrc+2) #.......................Colonna movb (1b+0) , %al movb (1b+1) , %ah movw %ax , (_vtrc+5) SC_WRITE _vtrc len_vtrc .endm Commento : La macro utilizza una stringa predefinita _vtrc che contiente la sequenza predefinita "\033[01;01H", con questa stringa salta alla locazione 1,1. Una stringa appunto è un array di caratteri : #.......1....34.67. .asciz "\033[01;01H" Quindi mi basta cambiare i numero relativamente agli indici 3,4 e 6,7 per ottenere una nuova sequenza di escape. Qui sorge un piccolo problema di programmazione, non volendo scrivere altre routine per trasformare un numero da decimale ad ascii, ho utilizzato un piccolo trucco. .macro vtrc _riga , _col .data 0: .ascii "\_riga" .byte '*' 1: .ascii "\_col" .byte '*' Le sequenze vengono passate alla macro come numeri, e questo non crea altro lavoro all'utente, la macro però vede esse come stringhe, in quanto le riceve tra i doppi apici “ ”. Quindi definisco delle variabili locali e in ordine memorizza la stringa. Ho utilizzato il carattere '*', in quantto se erroneamente l'utente fornisce un numero di una cifra esempio gotoxy 1,1 (che è corretto), la stringa memorizza il carattere nella prima posizione e non modifica la restante quindi, la sequenza di escape non viene interpretata regolarmente, gotoxy 01 , 01. Questo l'ho fatto per non scrivere altro codice. .text #.......................Riga movb (0b+0) , %al movb (0b+1) , %ah movw %ax , (_vtrc+2) #.......................Colonna movb (1b+0) , %al movb (1b+1) , %ah movw %ax , (_vtrc+5) SC_WRITE _vtrc len_vtrc Ancora il lavoro della macro non è finito, in quanto le stringhe vanno intepretare al contrario. Se il numero passato è 10, la stringa sarà “10” occorre a questo punto passare la sequenza alla stringa di escape che utilizzerò effettivamente modificare, e quindi il restante lavoro viene fatto da una semplice chiamata alla SYS_WRITE. Successivamente fornirò un esempio completo di tutte queste stringhe esempio : vtrg 01 01 Visualizziamo a colori ! Che mondo sarebbe senza i colori! ##################################################### VT 102 Colori # ##################################################### Back / Fore .equ .equ .equ .equ .equ .equ .equ .equ .equ .equ .equ vt_black vt_red vt_green vt_brown vt_blue vt_brown vt_blue vt_magenta vt_cyan vt_magenta vt_white , , , , , , , , , , , '0' '1' '2' '3' '4' '3' '4' '5' '6' '7' '8' # 30 fore ground , 40 background _vtcol: #...............colore generale .asciz "\033[30m" #...............lunghezza 6 pos 3,4 .equ len_vtcol , . - _vtcol .macro vtfore colore movb $\colore,%al movb %al,(_vtcol+3) movb $'3',%al movb %al,(_vtcol+2) write _vtcol , 5 .endm .macro vtback colore movb $\colore,%al movb %al,(_vtcol+3) movb $'4',%al movb %al,(_vtcol+2) write _vtcol , 5 .endm Commento : questa macro prende i riferimento i codici delle sequenze di escape prima esposti. I colori vanno da 0 a 8 meglio per quanto riguarda il terminale da 30 a 38 quelli in foreground e da 40 a 48 quelli in background. La sequenza è simile per entrambi ed è questa : _vtcol: #...............colore generale .asciz "\033[30m" Questa stringa definisce un generico colore (BLACK) di foreground. per quanto riguarda la gestione non dobbiamo fa altro che modificare la seconda parte della stringa con questo colore e riferirsi alla prima con un 3 o con un 4 a seconda dell'utilizzo che vogliamo farne (3=foreground) (4=background). esempio : vt vtfore vt_yellow Per quanto riguarda le altre sequenze ho assegnato direttamente una stringa di carattere e le visualizzo direttamente con la macro : vt {string} esempio : vt vtbold vt vtunderline_on ... vt vtunderline_off un banale esempio di utilizzo delle macro è rappresentato da questo semplici programma : .include "include/syscall.a" .include "include/vt102.s" .section .data .data outs: .asciz "\nHello World!\n" .equ len_outs , . - outs .text _start: ciclo: .globl _start movl $7,%ecx pushl %ecx addb $'0',%cl vtfore %cl SC_WRITE outs len_outs popl %ecx loopz ciclo vt vtreset xorl %ebx,%ebx SC_EXIT Input Prima abbiamo visto la chiamata di sistema per visualizzare un carattere sul terminale, ora vediamo la syscall per effettuare l'input da un terminale, molto semplice : buffer: .space 128 movl xorl movl movl int $3 ,%eax %ebx ,%ebx $buffer,%ecx $1 ,%edx $0x80 # sysread # stdin = 3 = 0 # car = 1 # linux syscall In questo caso abbiamo definito tramite la direttiva .space uno spazio di 128 caratteri, eccessivo per la restante parte del programma, in quanto si occupa di ricevere l'input da un carattere. La sys call read è ampiamente commentata e non ha bisogno di altre spiegazioni. Tuttavia ... Quando inserite un carattere da terminale, quest'ultimo attende la pressione del tasto 'RETURN' 'CHR$(13) nel vecchio basic' '\r', potrebbe andar bene all'occorrenza se dobbiamo inserire il nome, un numero ci sta il return dopo una sequenza, tuttavia risulta brutto dover aspettare la pressione del tasto return, in quanto spesso si ha la necessita di controllare, l'input o con una determinata sequenza di caratteri, oppure disabilitandone altri. La gestione del terminale in questo modo diventa più complicata, in quanto occorre gestirla a basso livello. Occorre attraverso un'ulteriore chiamata di sistema, dire al terminale di non aspettare la pressione del tasto return, quindi catturare l'input e ancora ripristinare la situazione di default del terminale. altrimenti dopo che il nostro programma sarà terminato altri che eseguiranno input all'interno della console, non attenderanno il tasto 'invio'. il file successivo si chiama getc.s (e colgo l'occasione in questa parte del libro di ringraziare FRANK KOTLER (comp.lang.asm.x86) per avermi aiutato quando al tempo ricercavo tale soluzione. Il programma è composto da una prima parte ove viene definita una struttura generale, che identifica i parametri del terminale, e di un variabile 'car1' di 4 byte , successivamente spiegherò il perchè di tale lunghezza. La parte principale del programma, non fa nient'altro che chiamare la routine 'getc', inizializzare il terminale, attendere la pressione di un tasto, quindi memorizzare il tasto nei due caratteri. Con la consueta sys call write vengono visualizzati sullo schermo. Fate Qualche prova e guardate l'output generato. Provate con in caratteri alfanumerici e poi con quelli speciali. Per i primi non ci sono particolari problemi, in quanto il terminale ritorna direttamente il tasto premuto, per quelli speciali il terminale ritorna come primo carattere una sequenza di escape. Notere che in memoria il carattere 'a' viene memorizzato come 'a' '\0' '\0' '\0 mentre il carattere per esempio 'INS' viene memorizzato come '\033' '[' '2' '~', il progrmma mette questi 4 valori in %eax per rendere più facili i confronti. .include "include/syscall.a" .section .data .align 4 termios_begin: termios_c_iflag: .long 1 # input mode flags termios_c_oflag: .long 1 # output mode flags termios_c_cflag: .long 1 # control mode flags termios_c_lflag: .long 1 # local mode flags termios_c_line: .byte 1 # line discipline term_c_cc: .space 19 # control characters termios_end: .equ termios_size , termios_end - termios_begin .data car1: .byte 0 .byte 0 .byte 0 .byte 0 .byte '\n' .equ .equ .equ .equ .equ NL STDIN STDOUT ICANON ECHO ,10 ,0 ,1 ,2 ,8 .equ .equ TCGETS ,0x5401 # tty-"magic" TCSETS ,0x5402 .equ .equ .equ .equ SYS_EXIT ,1 SYS_READ ,3 SYS_WRITE ,4 SYS_IOCTL ,54 #.Do erase and kill processing. #.Enable echo. .global getc .section .text getc: movl movl movl movl int $SYS_IOCTL $STDIN $TCGETS $termios_begin $0x80 andl ,%eax ,%ebx ,%ecx , %edx # get current mode $~(ICANON|ECHO),(termios_c_lflag) movl movl movl movl int $SYS_IOCTL $STDIN $TCSETS $termios_begin $0x80 movl movl movl movl int $SYS_READ $STDIN $car1 $4 $0x80 ,%eax ,%ebx ,%ecx ,%edx ,%eax ,%ebx ,%ecx ,%edx # just one , oppure sequenza # do it orl $(ICANON|ECHO),(termios_c_lflag) movl movl movl movl int $SYS_IOCTL ,%eax $STDIN ,%ebx $TCSETS ,%ecx $termios_begin ,%edx $0x80 movl (car1),%eax ret .text ############################################# .globl _start _start: call getc SC_WRITE car1 5 xorl %ebx movl $1 int $0x80 ,%ebx ,%eax Di seguito fornisco una tabella riportante i caratteri ANSI, potetee comqunque consultarli con il seguente comando : man console_codes . INS HOME PAG UP PAG DOWN ARROW UP ARROW DOWN ARROW RIGHT ARROW LEFT ESC ESC ESC ESC ESC ESC ESC ESC ESC ESC [2~ OH [5~ [6~ [A [B [C [D ESC F12 F11 ESC [ 2 4 ESC [ 2 3 F9 F8 F6 F6 F5 ESC ESC ESC ESC ESC F4 F3 F3 ESC [ O S ESC [ O R ESC [ O Q [20 [19 [18 [17 [15 esempio : .rodata KEY_F4: .string “\033[OS” .text cmpl (KEY_F4),%eax je ... La routine 'getc' ritorna la sequenza esatta del carattere premuto, in %eax, quindi per confrontare il carattere ho inizializzato delle stringhe di comodo. La libreria NCURSE S Ora che abbiamo visto la parte più grezza della gestione del terminale, possiamo fare una carrellata veloce della libreria NCURSE, appositamente studiata per lavorare in modalità testo. Vengono forniti solo alcuni esempi di base, per un approfondimento, come in altri parti del libro, riferirsi a documentazione più specifica. Esempio : The Hello World !!! #include <ncurses.h> int main() { initscr(); printw("Hello World !!!"); refresh(); getch(); endwin(); } /* /* /* /* /* Start curses mode Visualizza Hello World Visualizza sullo schermo “ premi un tasto “ fine */ */ */ */ */ return 0; debian:~/source# gcc -c prova.c -o prova.o debian:~/source# gcc prova.o -lc -lncurses -o main debian:~/source# ./main debian:~/source# Questo è un esempio di utilizzo del linguaggio 'C' e della libreria 'ncurses', nulla di particolare, una semplice compilazione e un link alle librerie. la controparte in assembly non differisce : .data outs: .string "\nHellow World " _start: .text .globl _start call initscr pushl $outs call printw addl $4,%esp call call call refresh getch endwin xorl %ebx ,%ebx movl $1 , %eax int $0x80 as --gstabs+ $1.s -o $1.o ld -dynamic-linker /lib/ld-linux.so.2 -o $1.bin $1.o -lc -lncurses La funzione initscr : inizializza il terminale in modalità 'ncurses'. Cancella lo schermo e presenta uno schermo vuoto. Questa funzione deve sempre essere chiamata per prima, in quanto inizializza la libreria ncurses ed alloca memoria per alcune strutture di utilizzo interno. refesh & printw : la funzione printw è usata come la normale printf al fine di visualizzare dei caratteri sullo schermo, tuttavia questa scrive in una finestra immaginaria e solo dopo la chiamata a refresh i caratteri vengono visualizzati correttamente sullo schermo. endwin : infine questa funzione libera la memoria da tutte le strutture allocate precedentemente. Per utilizzare la libreria occorre chiamare le opportune funzioni con i rispettivi parametri, possibilmente cercando di utilizzare i paramentri generali della stessa, cosa un po' più complicata. Del resto poi è come programmarla in 'C'. Qui di seguito fornisco una rapida carrellata di alcune funzioni della libreria, non era nello scopo del capitolo inserirla, ma per completezza mi sembrava opportuna, quantomeno sapere della sua esistenza per programmare intercacce testuali, senza dover sta li a riscrivere un sacco di codice. Ulteriori funzioni su ncurses : .data ch: .long 0 outs: .string "Premi qualsiasi carattere e lo visualizzo in BOLD" outch: .string " %c " .text .globl _start _start: call initscr call cbreak # Start curses mode # disabilita 'enter' in input call noecho # non visualizzare il carattere premuto pushl $outs call printw addl $4,%esp call getch movl %eax,(ch) # se non abbiamo chiamato raw # dovremo premere 'enter' pushl $1<<21 call attron popl %eax # attributo BOLD pushl (ch) pushl $outch call printw addl $8,%esp pushl $1<<21 call attroff popl %eax # attributi normali call refresh call getch call endwin # Print it on to the real screen # Wait for user input # End curses mode xorl %ebx,%ebx movl $1,%eax int $0x80 Non mi soffermero più di tanto a commentare il programma. I colori sono definiti nell'header 'curses.h' in questo modo : #define #define #define #define #define #define #define #define ... COLOR_BLACK COLOR_RED COLOR_GREEN COLOR_YELLOW COLOR_BLUE COLOR_MAGENTA COLOR_CYAN COLOR_WHITE 0 1 2 3 4 5 6 7 Segue una definizione relativamente ai tasti premuti #define KEY_DOWN 0402 /* down-arrow key */ #define KEY_UP 0403 /* up-arrow key */ #define KEY_LEFT 0404 /* left-arrow key */ #define KEY_RIGHT 0405 /* right-arrow key */ #define KEY_HOME 0406 /* home key */ #define KEY_BACKSPACE 0407 /* backspace key */ #define KEY_F0 0410 /* Function keys. Space for 64 */ #define KEY_F(n) (KEY_F0+(n)) /* Value of function key n */ #define KEY_DL 0510 /* delete-line key */ #define KEY_IL 0511 /* insert-line key */ #define KEY_DC 0512 /* delete-character key */ #define KEY_IC 0513 /* insert-character key */ #define KEY_EIC 0514 ... questo è un esempio di F1 = 0411 #define NCURSE S_ATTR_SHIFT 8 #define NCURSE S_BITS(mask,shift) ((mask) << ((shift) + NCURSE S_ATTR_SHIFT)) #define #define #define #define #define #define #define #define #define #define ... A_NORMAL 0L A_ATTRIBUTES NCURSES_BITS(~(1UL - 1UL),0) A_CHARTEXT (NCURSES_BITS(1UL,0) - 1UL) A_COLOR NCURSES_BITS(((1UL) << 8) - 1UL,0) A_STANDOUT NCURSES_BITS(1UL,8) A_UNDERLINE NCURSES_BITS(1UL,9) A_REVERSE NCURSES_BITS(1UL,10) A_BLINK NCURSES_BITS(1UL,11) A_DIM NCURSES_BITS(1UL,12) A_BOLD NCURSES_BITS(1UL,13) per quanto riguarda gli attributi la formula scritta sopra in alto è la seguente pushl $1<<21 # mask << shit + Attr_BOLD # 1 << 8 13 Apriamo una finestra sul mondo Ma ?! mi sembra di averla già sentita questa frase. Un'ultima analisi delle funzioni della libreria. Lascio a voi investigare se di vostro interesse, l'uso dei pannelli e dei menu e delle altre funzioni avanzate. .data outwin: .string "Hello World!" pwin: .long 0x0 _start: .text .globl _start call initscr # Start curses mode pushl $5 pushl $5 pushl $20 pushl $10 call newwin addl $16,%esp #x #y # width # height movl %eax,(pwin) # salva il puntatore alla window pushl $0 pushl $0 pushl (pwin) call box addl $12,%esp # x funzione box #y pushl $outwin pushl $1 pushl $1 pushl (pwin) call mvwprintw addl $16,%esp # print in windows (mvwprintw ( pwin ,1 ,1 ,”Hello...” ) ; # x interno alla finestra # y interna alla finestra # puntatore alla finestra call refresh # refresh screen pushl (pwin) call wrefresh addl $4,%esp # refresh win pushl (pwin) call delwin addl $4,%esp # destructor libera la memoria della struttura allocata call getch call endwin xorl %ebx,%ebx movl $1,%eax int $0x80 # get char # end ncurses mode # termina programma come consuetudine CAPITOLO 13 L'Elfo e Hex L'Elfo e Hex Sembra più un racconto fantasi che l'introduzione ad un capitolo di programmazione. Comunque volevo accennare ad un formato degli eseguibili su linux : l'' ELF. Le descrizioni piuttosto approsimative, in quanto lascio al lettore la scelta di approfondire gli argomenti. Unitamente alla spiegazione dei due programmi introduco alcuni concetti di programmazione, quali le maschere, l'allocazione di memoria, la gestione dei file ed altro, di seguito descriverò il programma passo passo, voi copiatelo così. Nel capitolo precedente avevo accennato al solo utilizzo dell'assembly e non alle funzioni 'C', beh una tirata di orecchi me la merito ora; tuttavia il mio scopo era quello di illustrare altri concetti e mi risultava comodo utilizzare una funzione 'C' che simulasse la ATOI. partiamo ... .include "include/syscall.a" .include "include/stdlibc.a" .data Di seguito viene riportata una nuova macro che utilizzerò pigramente più avanti nel programma per visualizzare la maschera dati. Questa macro si occupa di trasformare un numero esadecimale in una stringa di caratteri al fine di poter essere visualizzato; potete verificare la validità del programma con il comandi : readelf -h nomeprogramma. La versione su linux visualizza i numeri esadecimali, noi li visualizziamo in decimale. La macro xtoa fa uso delle espressioni condizionali al suo interno : .ifc questa istruzione confronta due stringhe ed esegue il contenuto se queste sono uguali. 'n' rappresenta il numero di byte da convertire rispettivamente : 'byte' , 'word' e 'long'. Ancora fa uso di una variabile locale dove memorizza il formato di visualizzazione “%x” utilizzato dalla sprintf. Sintassi : sprintf sprintf ( numero , formato , sdestinazione ) ( $10,”%x”,stringa ) ; Quindi la macro ripete le tre condizioni per impostare il codice a seconda di byte,word o long. ################################################ xtoa int string byte ################################################ .macro xtoa itoan itoasdest n="1" xorl %eax,%eax .ifc "\n" , "1" .data 0: .string "%x" .text movb (\itoan),%al pushl %eax pushl $0b .endif .ifc "\n" , "2" .data 0: .string "%02x" .text movw (\itoan),%ax pushl %eax pushl $0b .endif .ifc "\n" , "4" .data 0: .string "%04x" .text movl (\itoan),%eax pushl %eax pushl $0b .endif pushl \itoasdest call sprintf addl $12,%esp .endm Nulla di particolare da aggiungere alla funzione #...................... input file handler fin: .long 0 #................................ elf header Questa parte dichiara un puntatore ad un file, linux memorizza i file come 'inode' come numeri per poter poi far riferimento. fin = file input. La parte da elf_begin a elf_end, illustra la struttura di testa del file in formato ELF indicando il significato dei rispettivi byte. Tutti i file in formato ELF iniziano con 0x7f E L F questa stringa dei primi 4 caratteri identifica che si tratta di un file in formato ELF. (questo numero viene chiamato ('magic number'). Un file ELF consite di un header seguito dalla program header table e dalla section header table. L'header come dice la parola viene sempre messo in testa, per le restanti sezioni è il file di testa 'header' che definisce la posizione delle altre. Le altre due sezioni descrivono le peculiarità del file ELF. .data elf_begin: elf_ident: .byte 0 .byte 0 .byte 0 .byte 0 elf_class: .byte 0 elf_data: .byte 0 elf_version0: .byte 0 elf_magic_number: # 9 byte .quad 0 .byte 0 elf_type: .word 0 elf_machine: .word 0 elf_version1: .long 0 elf_entry_point: .long 0 elf_phoff: .long 0 elf_shoff: .long 0 elf_flags: .long 0 elf_hsize: .word 0 elf_phent_size: .word 0 elf_phnum: .word 0 elf_shent_size: .word 0 elf_shnum: .word 0 elf_shstrndx: .word 0 elf_end: .equ elf_size , . - elf_begin La prima parte del file di testa è identificata da questa struttura. Maggiori informaizoni : man ELF. importante questa struttura deve essere allineata a 4 “.align 4” #define EI_NIDENT 16 typedef struct { unsigned char uint16_t uint16_t uint32_t ElfN_Addr ElfN_Off ElfN_Off uint32_t uint16_t uint16_t uint16_t uint16_t uint16_t uint16_t } ElfN_Ehdr; e_ident[EI_NIDENT]; e_type; e_machine; e_version; e_entry; e_phoff; e_shoff; e_flags; e_ehsize; e_phentsize; e_phnum; e_shentsize; e_shnum; e_shstrndx; Non riporto per esteso il significato dei singoli byte, in quanto sono chiaramente spiegati nel manuale. Il programma si preoccupa solo di visualizzare l'header di un file ELF. Maggiori informazioni vedi comandi : readelf. .data sintassi: .asciz "\n ? Sintassi : Leggielf <nome programma>\n" .equ len_sintassi , . - sintassi newline: .asciz "\n" .equ len_newline , . - newline Qui vengono specificate le stringhe di output utilizzate dalla syscall write. Il punto che vedete dopo .equ sta a significare l'esatta posizione (indirizzo) quindi sottraendo l'etichetta iniziale della stringa ne ottengo la lunghezza, che utilizzerò poi per la syscall. .text .globl _start _start: pushl %ebp movl %esp,%ebp In questo caso si tratta di una normale routine in apertura, salva i parametri utilizzti, essendo questo considerata al pari di una main, risulta superfluo. movl st_argc(%ebp),%eax Qui vengono prelevati dallo stack i dati forniti dalla riga di comando .equ st_argc .equ st_argv0 .equ st_argv1 , , , 4 8 12 # numero argomenti # nome programma # argomento passato Alla posizione 4 per cosi dire, lo stack punta ad un long contenente il numero di argomenti passati dalla linea di comando. argv0 indetifica il nome del programma se arv0=0 ovviamente non sono stati forniti parametri in input. argv1 identifica il primo argomento e così via. cmpl $1,%eax jne continua SC_WRITE sintassi , len_sintassi # visualizza sintassi jmp Esci continua: Questa parte visualizza il nome del file, in queto caso ho chiamato la routine puts, in quanto riconosce il carattere terminatore NULL '\0' e gestisce correttamente la stringa. SC_WRITE newline , len_newline movl st_argv1(%ebp),%eax pushl %eax call puts popl %eax Questa è la classica fopen del 'C' la syscall è la numero 5 e richiede in input il nome del file, la modalità con cui viene aperto RO (read Only) solo lettura ed i permetti, deviniti in ottale. Se l'operazione è andata a buon fine, %eax puntera correttamente all'inode del file, altrimenti ritornera 0. La fopen utilizza queste direttive in apertura vedi : man fopen r r+ w w+ a a+ b = = = = = = = read read & write truncate write create write append append reading, write eof binary 0 03101 per quanto riguarda i permessi occorre prendere in considerazione la numerazione ottale ed i proprietari : proprietario gruppo r 4 r 4 w 2 x 1 w 2 altri x 1 r 4 w 2 x 1 da questa tabella è possibile evincere il “number of the beast” 666.I numeri in ottale sono la rispettiva somma dei numeri sopra riportati, in queto caso il file avrà per tutti i permessi di lettura e scrittura,ma non di esecuzione. movl $RO #........................................ open movl st_argv1(%ebp) , %ebx , %ecx # fa riferimento alla open del c movl $0666 , %edx # permessi movl $SYS_OPEN , %eax int $SYSCALL #........................... salva handler file movl %eax,(fin) La stessa syscall READ duttile, viene utilizzata per leggere da un file, in questo caso il buffer di input non è STDIN ma il file stesso. La syscall READ richiede l'inode del file, l'inizio di un area di memoria dove memorizzare i byte letti, la quantità di byte da leggere. Come potete vedere, questa sys read inizializza correttamente tutti i campi della struttura dell'header file di ELF. #.................................... Leggi ELF movl (fin) , %ebx movl $elf_begin , %ecx movl $elf_size , %edx movl $SYS_READ , %eax int $SYSCALL ###################################### jne Esci controlla magic number movb $0x7f , %al cmpb (elf_ident) ,%al # questo file non è ELF Ogni file di tipo ELF in testa presenta dei numeri magici come accennato precedentemente, se il primo ed i successivi non sono 0x7F E L F, sicuramente non si tratta di un file di tipo ELF. Questa parte di programma non fa altro che copiare il contenuto della struttura, in un'altra parte della memoria definita come maschera di visualizzazione. Anche Questa seconda maschera viene gestita dalla syscall write e utilizza l'indirizzo iniziale e la sua lunghezza. Questa parte di programma non fa nient'altro che convertire i valori esadecimali instringhe ascii al fine di poterli visualizzare. ################################### Converti # scrive il dato letto dall'header elf nella stringa di visualizzazione xtoa elf_class prova: $welf_class 1 xtoa elf_data $welf_data xtoa elf_version0 $welf_version0 1 1 xtoa elf_type xtoa elf_type 2 2 $welf_type $welf_type xtoa elf_machine $welf_machine xtoa elf_version1 $welf_version1 2 2 xtoa elf_entry_point xtoa elf_phoff xtoa elf_shoff 4 4 4 xtoa elf_flags xtoa elf_hsize $welf_entry_point $welf_phoff $welf_shoff $welf_flags $welf_hsize xtoa elf_phent_size $welf_phent_size xtoa elf_phnum $welf_phnum xtoa elf_shent_size $welf_shent_size xtoa elf_shnum $welf_shnum xtoa elf_shstrndx $welf_shstrndx xtoa elf_magic_number xtoa elf_magic_number+9 2 4 2 2 2 2 2 $welf_magic_number 4 $welf_magic_number+9 1 Visualizza la maschera di output formattata. SC_WRITE welf_begin , welf_size Esci: #........................... close file movl $6 ,$eax movl (fin),%ebx int $0x80 movl %ebp,%esp popl %ebp xorl %ebx,%ebx movl $1,%eax int $SYSCALL Una simpatica caratteristica della maschera di visualizzazione è rappresentata dai caratteri speciali del terminale. Come potete vedere sono inframezzati tra un testo e l'altro allo scopo di colorare ed evidenziare le classi dei dati. Una nuova direttiva di gas è rappresentata da .space. Sintassi .space numero , carattere. La quale riempe un area di memoria di tanti caratteri specificati da numero. #################################################### .data .macro blu .ascii "\033[34m" .endm .macro bianco .ascii "\033[37m" .endm .macro reset .ascii "\033[0m" .endm welf_begin: # è una sequenza di stringhe blu .string "\n ELF Header.........: " , bianco .string "0x7F E L F" welf_ident: .space 4 blu .string " \n class..............: " bianco welf_class: .space 4 blu .string "\n data...............: " bianco welf_data: .space 4 blu .string "\n Version............: " bianco welf_version0: .space 4 blu .string "\n Elf Magic number...: " bianco welf_magic_number: .space 20 blu .string "\n Elf type...........: " bianco welf_type: .space 5 blu .string "\n Elf Machine........: " bianco welf_machine: .space 5 blu .string "\n Elf Version........: " bianco welf_version1: .space 5 blu .string "\n Elf entry point....: " bianco welf_entry_point: .space 9 blu .string "\n phoff..............: " bianco welf_phoff: .space 9 blu .string "\n shoff..............: " bianco welf_shoff: .space 9 blu .string "\n flags..............: " bianco welf_flags: .space 9 blu .string "\n hedear size........: " bianco welf_hsize: .space 5 blu .string "\n program size.......: " bianco welf_phent_size: .space 5 blu .string "\n phnum..............: " bianco welf_phnum: .space 5 blu .string "\n sh ent size........: " bianco welf_shent_size: .space 5 blu .string "\n shnum..............: " bianco welf_shnum: .space 5 blu .string "\n string index.......: " bianco welf_shstrndx: .space 5 .byte '\n' reset welf_end: .equ welf_size , . - welf_begin Ultimo carattere speciale 'reset' che riporta il terminale allo stato iniziale. BRK Facciamo un break! Come avete visto dal programma precedente possiamo allocare i dati attraverso, la sezione .data, oppure attraverso la sezione .bss,tuttavia nella programmazione non sempre è possibile sapere a priori se non in run-time l'esatto ammontare delle dimesioni dell'array da allocare. Qui ci vengono in aiuto le funzioni della libreria 'C' come malloc/free. E' possibile allocarle direttamente usufruendo dell'aiuto della system call brk. Questa syscall ci ritorna il primo blocco di memoria libera, da qui è possibile ricercare i gruppi di pagine contigui al fine di allocare la dimensione da noi richiesta. Tuttavia, non è da preferire questa soluzione, a meno che la routine di allocazione sia effettivamente efficente da rimpiazzare quelle della libreria c. Osservate questo esempio : ################# # # Allocate init # ################# # inizializza la routine di allocazione .data .equ BRK,45 current_break: .long 0 heap_begin: .long 0 # syscall brk # ultimo indirizzo valido # il nostro primo indirizzo valido .text .globl allocate_init .type allocate_init,@function allocate_init: pushl %ebp movl %esp , %ebp movl xorl int $BRK , %eax %ebx , %ebx $0x80 incl movl movl %eax %eax,current_break %eax,heap_begin movl popl ret %ebp,%esp %ebp # invoca la system call brk # %eax = l'ultimo indirizzo valido # salva l'ultimo indirizzo valido # memorizza l'ultimo indirizzo # come il nostro primo valido Come visto dai commenti, queta routine ha il solo scopo di inizializzare la funzione che vedremo più avanti di allocazione della memoria. ################# # # Allocate # ################# # alloca la memoria .equ HEADER_SIZE , 8 # caratteristiche dei primi 8 byte # di un blocco di memoria in linux .equ .equ HDR_AVAIL_OFFSET HDR_SIZE_OFFSET , , 4 0 # disponibilità # dimensioni .globl allocate .type allocate,@function allocate: pushl %ebp movl %esp , %ebp movl 8(%ebp) , %ecx movl heap_begin , %eax movl current_break, %ebx alloca_loop_begin: cmpl je # %ecx = numero di byte da allocare # %eax = prima posizione disponibile # %ebx = break point attuale # { %ebx,%eax move_break # se questo blocco non è disponibile passa al successivo movl HDR_SIZE_OFFSET(%eax) cmpl $0 je next_location , , %edx HDR_AVAIL_OFFSET(%eax) # se lo spazio è disponibile, verifica se c'è abbastanza spazio # per la richiesta di allocazione (%ecx=numero di byte da allocare) cmpl jle %edx , %ecx allocate_here # se c'è abbastanza spazio alloca next_location: addl addl # # # # # $HEADER_SIZE %edx , , %eax %eax il prossimo blocco di memoria libera è dato dalle dimensioni del blocco dimemoria attuale %edx, più le dimensioni del blocco preso in esame (8 byte) : 0 – 3 disponibilità 4 – 7 dimensioni jmp allocate_loop_begin # } allocate_here: movl $0,HDR_AVAIL_OFFSET (%eax) # ora lo spazio deve risultare non più # disponibile addl $8, %eax # punta al blocco successivo movl %ebp,%esp ret # La routine di allocazione vera e propria è molto banale, da come potete vedere in questo esempio. # in questo caso dobbiamo ricercare un blocco di memoria che possa ospitare # le dimensioni della nostra richiesta. move_break: addl addl $HEADER_SIZE,%ebx %ecx ,%ebx # ora %ebx ospita il nuovo break point pushl %eax pushl %ecx pushl %ebx movl int $BRK,%eax $0x80 richieste cmpl richieste je popl popl popl $0,%eax # richiediamo un nuovo break point # %eax=0 --> FAIL ! # %ebx contiene le dimensioni da noi # da noi come non %ebx contiene le dimensioni error %ebx %ecx %eax movl $0 disponibile movl %ecx addl $8 movl %ebx , HDR_AVAIL_OFFSET(%eax) , HDR_SIZE_OFFSET(%eax) , %eax , current_break movl %ebp , %esp popl %ebp ret # marca il blocco # memorizza la dimensione del blocco # %eax contiene il prossimo blocco # memorizza il nuovo break point ################# # # Deallocate # ################# # Dealloca la memoria .globl deallocate .type deallocate,@function deallocate: movl 4(%esp) , %eax # 1° parametro = indirizzo della memoria # da rendere disponibile subl $8 , %eax # %eax ora è all'inizio del blocco # punta alle informazioni movl $1 , HDR_AVAIL_OFFSET(%eax) # marcalo disponibile ret Ho preferito passare ad una spiegazione, delle routine commentate per facilitare o non, la comprensione del funzionamente dell'allocazione della memoria. Desidero ringraziare Jonathan Barlett ed il suo meraviglioso libro “Programming from the ground up”, che per primo mi ha introdotto nella programmazione in assembly in ambiente linux. A questo punto i lettori più attenti avranno certamente notato il fatto, che manca qualsiasi alluzione a Hex, come promesso nel titolo. Bene Hex è scappato via, meglio era il sorgento del programma HexEdit che vedrete nel capitolo 25, relativamente alla prte sul disassembling. Avevo scritto circa 8 pagine, che sono poi finite da qualche parte nei terabyte dei miei dischi o nei copia incolla e ancora saranno sospese a mezz'aria , detto il fatto non ho più riscritto questa parte. sigh ... :-( ! Make Quando i programmi si fanno complessi, e quindi composti da diversi centinaia di file, sarebbe improponibile dove ricompilare tutto ogni volta, pensate solo all'enorme spreco di risorse e di tempo, in nostro aiuto è stata sviluppata l'utility 'make' che legge da un file 'Makefile' le regole che noi gli forniamo per la compilazione e linking del nostro programma. Questa utiliti va a guardare la data dell'ultima modifica del codice sorgente e dell'oggetto se risulta uguale non viene ricompilato ilsorgente ma linkato direttamente ocn il comando link agli altri moduli. Il vantaggio no termina qui, inquanto è possibile indicare al make di generare diversi tipi di output, possiamo costruire la nostra 'release' oppure programmin con le opzioni di debug, possiamo utilizzare delle opzioni come 'clean' e 'build' che puliscono tutto il nostro codice e lo ricodificano da zero, riusciamo a fare anche l'installazione dei programmi e comodamente gli passamio i parametri che più ci aggradano. Una vera utilità vediamo un esempio : ####################################### # # programma calcolo Postfix Infix Expr # ###################################### # stringa ottimizzazione compilatore CFLAGS=-O2 CC=gcc SRCS=prog1.c prog2.s OBJS=progr1.o progr2.o MAIN=main # ............................... lika oggetti all: $(CC) $(CFLAGS) $(SRCS) -o $(MAIN) #................. regole costruzione obj file dieci.o : $(CC) $(CFLAGS) progr1.c -o progr1.o a10.o : $(CC) $(CFLAGS) progr2.s -o progr2.o #............................... pulisci tutti clean: rm *.o Questo è un semplice Makefile con delle regole per costruire i due moduli. Lanciando Make, questa andrà a riferirsi all'etichetta all: e quindi alla compilazione dei due file sorgenti attraverso l'indicazione di source. Altre opzione che possiamo utilizzare è make clean che ripulisce la cartella dagli obj per poterli ricompialre. Possiamo aggiungere anche make install con le istruzioni per copiare il programma in una nostra directory. E' possibile sovrascrivere anche i flag a nostro piacimento. Se non abbiamo previsto un'opzione di debug come nel nostro caso possiamo digitare la seguente linea di comando : – make CFLAGS=-g -O2 I parametri verranno sovracritti e generato un nuovo eseguibile questa volta con le istruzioni di debug. CAPITOLO 14 Le istruzioni Multimediali Le istruzioni multimediali Prima di addentrarci nello studio delle istruzioni multimediali davvero tante, occorre capire come sono strutturare le parole riservate del compilatore (mnemonici) per meglio apprendere e non confonderci con tali istruzioni. Le istruzioni in un certo senso sono autoesplicative, benchè composte da consonati che a prima vista non sono per niente intuitive. Per esempio questo mnemonico “cvtdqtps ” a prima vista non risulta per niente facile capire cosa esattamente fa, ma dopo la nostra tabellina ci risulterà un po' più chiaro , le consonanti sono contratture di temini e significati più ampi : – – – – – – – – – – – – – – – – – – – – – – – – CVT CVTT P PACK PUNPCK UNPCK B D DQ H L PD PI PS Q R S SD SI SS U US W x : : : : : : : : : : : : : : : : : : : : : : : : Convert Converto with truncation Packed pack element 1x,2x data size unpacked and interleave element unpacked and interleave element byte Double Word Double Quad Word high Low, Left packed double precision floating point packed integer packed single precision floating point QuadWord Right Signed, Saturation, Shift Scalar Double precision floating point Scalar integer Scalar Single Precision floating point unsigned, unordered, unaligned Unsigned saturation Word uno o più caratteri nello mnemonico Qundi ritornaniamo alla nostra istruzione : cvtdqtps cvt dq t ps Convert Data Quad Word With truncation to packed single precision floating point In teoria, poi la vediamo in pratica, se tutto quello che ho detto è corretto, questa istruzione dovrebbe convertire un numero da 64 bit (inteso come double) a 32 bit (floating point, packed) with truncation. MMX Iniziero ora lo studio delle 57 istruzioni Multimediali MMX , come introduzione alla grafica.Questo set di istruzioni è stato introdotto dal modello Pentium in poi quindi per i modelli precedenti queste non sono disponibili, quindi ora vi pongo un problema come determinare di che tipo di macchina si tratta e se questa supporta le istruzioni ? Possiamo interrogare la macchina tramite l'istruzione CPUID e vedere se il bit 23 è settato a 1. Desisdero ringraziare Randall Hyde (webster University – the art of programming assembly), che grazie ai suo modo di insegnare ho potuto apprendere l'arte della programmazione in linguaggio macchina. Questo capitolo lo dedico interamente al prof., per maggiori informazioni consultate il sito online della e approfondite i trattati con i suoi libri. Vedi HLA High Level Assembly. esempio : movl %1 cpuid test $800000 jnz MMX ,%eax # richiesta dei flag ,%edx # $0080:0000 # MMX supportate Le istruzioni multimediali si avvalgono di 8 registri a 64 bit in floating point : MM0 – MM7 (64 bit) questi registri sono l'equivalente della parte bassa dei rispettivi registri del coprocessore matematico st0 – st7 a 80 bit Registri ST Registri MMX ST0 80 BIT MM0 64 BIT ST1 80 BIT MM1 64 BIT ST2 80 BIT MM2 64 BIT ST3 80 BIT MM3 64 BIT ST4 80 BIT MM4 64 BIT ST5 80 BIT MM5 64 BIT ST6 80 BIT MM6 64 BIT ST7 80 BIT MM7 64 BIT N.B. benchè i registri FPU e MMX occupino la medesima area di memoria, i set di tali istruzioni non possono essere mischiate per effettuare i calcoli, occorre utilizzare alternativamente o una o l'altro set. Una speciale istruzione EMMS (exit MMS) resetta la cpu ed è così possibile iniziare i calcoli con la FPU. Ogni registro può essere ulteriormente suddiviso in parti più piccole, come il registro EAX (32 bit), troviamo AX (16 bit) e ah al (8 bit) anche per i registri MMX possiamo suddividerli in byte, word, dword e qword. MMX 64 BIT 1 BYTE 1 BYTE 1 BYTE 1 WORD 1 BYTE 1 BYTE 1 WORD 1 BYTE 1 WORD 1 DOUBLE WORD 1 BYTE 1 BYTE 1 WORD 1 DOUBLE WORD 1 QUAD WORD Benchè mmx estendono i registri a 64 bit, questo vengono usate solo a scopo multimediale per velocizzare le operazioni, in quanto questo particolare set può lavorare parallelamente sui dati. Ancora il set di istruzioni MMX supporta la saturazione aritmetica, (Sign Ext, Zero Ext), cioè se prendiamo un piccolo dato in esame un byte FF ed aggiungiamo 1 questo provocherà un overflow 0b0100 ed un riporto di carry con bit di segno a 1; questo avviene perchè MMX fa riferimento alla dimensione (size) dell'operando preso in esame al momento, benchè i registri siano a 64 bit. Nella fattispecie la sintassi generale delle istruzioni è composta da un operando sorgente e da uno di destinazione (generalmente un registro) a meno che il risultato non venga scritto in memoria, oppure con un terzo elemento un parametro costante che non può essere un valore a 64 bit per esempio nelle operazioni di shift e rotazione. sintassi : MMX Istr MMX istr Sorgente Sorgente , Destinazione , Destinazione , Costante Anche se la sorgente è un operatore a 4 byte una quad o word occorre specificare che il dato può benissimo essere il singolo byte visto che questo set può lavorare parallelamente sui dati. Ancora il set di istruzioni utilizza gli indirizzamenti possibili del x86, quelli utilizzati e visto fino ad ora. Il set di istruzioni multimediali può essere suddiviso in : – – – – – – – Trasferimento dati ; Conversione ; Packed ; Confronti ; Logiche ; Shift e Rotazione ; EMMS. nella forma generale le istruzioni multimediali iniziano sempre con 'P', poi hanno un suffisso che identifica il tipo di operazio (add) ed infine la dimensione del dato (w) : Sintassi generale : - P ADD W (paddw) Trasferimento di dati .data qvar: .quad 0x01234abcd dvar: .quad 0x5678 .text .globl _start _start: movq (qvar) , %mm0 movd (dvar) , %mm1 movl $1,%eax int $0x80 Questo piccolo programma mostra come utilizzare i registri mmx caricandome in memoria i vari valori. riporto anche l'output di GDB, notate come tratta i dati come se fossere contemporameamente qword, dword, word o byte. La visualizzazione del secondo registro l'ho midificata per maggioe chiarezza espositiva. (gdb) p/x $mm0 $1 = {uint64 = 0x1234abcd, v2_int32 = {0x1234abcd, 0x0}, v4_int16 = {0xabcd, 0x1234, 0x0, 0x0}, v8_int8 = {0xcd, 0xab, 0x34, 0x12, 0x0, 0x0, 0x0, 0x0}} (gdb) p/x $mm1 $2 = { uint64 = 0x5678, v2_int32 = {0x5678, 0x0}, v4_int16 = {0x5678, 0x0, 0x0,0x0}, v8_int8 = {0x78 , 0x56, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0} } (gdb) Istruzioni Aritmetiche Riporto per esteso il file con i relativi commenti di output dei registri. .data qvar1: .quad 0xffffffffffffffff qvar2: .quad 0x0000000000000001 .text _start: .globl _start movq (qvar1) , %mm0 movq (qvar2) , %mm1 paddb %mm0 , %mm1 # ffff:ffff:ffff:ffff # 0000:0000:0000:0001 # ffff:ffff:ffff:ff00 movq (qvar1) , %mm0 movq (qvar2) , %mm1 paddw %mm0 , %mm1 # ffff:ffff:ffff:ffff # 0000:0000:0000:0001 # ffff:ffff:ffff:0000 movq (qvar1) , %mm0 movq (qvar2) , %mm1 paddd %mm0 , %mm1 # ffff:ffff:ffff:ffff # 0000:0000:0000:0001 # ffff:ffff:0000:0000 movq (qvar1) , %mm0 movq (qvar2) , %mm1 paddq %mm0 , %mm1 # ffff:ffff:ffff:ffff # 0000:0000:0000:0001 # 0000:0000:0000:0000 movl $1,%eax int $0x80 Quando lavorate con MMX dovete pensare in modo parallelo in effetti, la forza di queste istruzioni è questa, gestire più dati contemporaneamente; dal listato sopra riportato potete notare come le istruzioni facciano riferimento ai byte, word, dword o qword; quindi come in questo caso è possibile lavorare 8 byte alla volta oppure 4 word, ancora 2 double word infine una quad word simultaneamente. La somma come vedete non influisce con il carry al dato successivo, per quanto riguarda l'esempio dei byte, si interessa solo del byte corrispondente. Le istruzione che sottraggono i byte funzionano in modo analogo alle precedenti, non prendendo a prestito (SBB) il carry dal numero precedente o addizionando il carry (adc) al byte successivo. .data qvar2: .quad 0x0000000000000000 qvar1: .quad 0x0000000000000001 .text _start: .globl _start movq (qvar1) , %mm0 movq (qvar2) , %mm1 psubb %mm0 , %mm1 # 0000:0000:0000:0000 # 0000:0000:0000:0001 # 0000:0000:0000:00ff movq (qvar1) , %mm0 movq (qvar2) , %mm1 psubw %mm0 , %mm1 # 0000:0000:0000:0000 # 0000:0000:0000:0001 # 0000:0000:0000:ffff movq (qvar1) , %mm0 movq (qvar2) , %mm1 psubd %mm0 , %mm1 # 0000:0000:0000:0000 # 0000:0000:0000:0001 # 0000:0000:ffff:ffff movq (qvar1) , %mm0 movq (qvar2) , %mm1 psubq %mm0 , %mm1 # 0000:0000:0000:0000 # 0000:0000:0000:0001 # ffff:ffff:ffff:ffff movl $1,%eax int $0x80 Come potete vedere esegue la sottrazione senza eseguire un riporto. La moltiplicazione funziona differentemente dalle due, in quanto tiene conto del wraparound mode, quindi occorre calcorare prima la parte bassa poi quella alta. Queste viene fatto, per evitare gli overflow .data qvar1: .quad 0x0000000000001234 qvar2: .quad 0x0000000000001000 .text _start: .globl _start movq (qvar1) , %mm0 movq (qvar2) , %mm1 pmullw %mm0 , %mm1 # 0000:0000:0000:1234 # 0000:0000:0000:1000 # 0000:0000:0000:4000 movq (qvar1) , %mm0 movq (qvar2) , %mm1 pmulhw %mm0 , %mm1 # 0000:0000:0000:1234 # 0000:0000:0000:1000 # 0000:0000:0000:0123 movl $1,%eax int $0x80 Come vedete dal listato, viene calcolato alternativamente la parte bassa e poi bassa della aprola. Packed multiply low word. AND OR XOR Le istruzioni logighe funzionanano esattamente come le cugine AND OR NOT qui vi riporto un listato con i rispettivi esempi : .data qvar1: .quad 0xffffffffffffffff qvar2: .quad 0x0101010101010101 .text _start: .globl _start movq (qvar1) , %mm0 movq (qvar2) , %mm1 pand %mm0 , %mm1 # ffff:ffff:ffff:ffff # 0101:0101:0101:0101 # 0101:0101:0101:0101 movq (qvar1) , %mm0 movq (qvar2) , %mm1 pxor %mm0 , %mm1 # ffff:ffff:ffff:ffff # 0101:0101:0101:0101 # fefe:fefe:fefe:fefe movq (qvar1) , %mm0 movq (qvar2) , %mm1 por %mm0 , %mm1 # ffff:ffff:ffff:ffff # 0101:0101:0101:0101 # ffff:ffff:ffff:ffff movl $1,%eax int $0x80 Confronto Le istruzioni di confronto sono essenzialmente 2 : – – GT greater ; EQ equal. Per ottenere le altre occorre simularle, per esempio se A > B allora B < A, occorre invertire gli operandi. Per la disegualgianza occorre fare l'operazione di ugualglianza ed invertire tutti i bit (NOT). .data qvar1: .quad 0xffffffffffffffff qvar2: .quad 0xffff0010ff00ff01 qtmp: .quad 0x0 .text _start: .globl _start # ? qvar1 = qvar2 movq (qvar1) , %mm0 movq (qvar2) , %mm1 pcmpeqb %mm0 , %mm1 # ffff:ffff:ffff:ffff # ffff:0010:ff00:ff01 # ffff:00ff:ff00:ff00 # ? qvar1 != qvar2 movq (qvar1) , movq (qvar2) , pcmpeqb %mm0 , movq %mm1 , movl (qtmp) , not %eax movl %eax , movl (qtmp+4), not %eax movl %eax , movq (qtmp) , %mm0 %mm1 %mm1 (qtmp) %eax (qtmp) %eax (qtmp+4) %mm1 # ? qvar1 > qvar1 movq (qvar1) , %mm0 movq (qvar2) , %mm1 pcmpgtb %mm0 ,%mm1 # ? avar1 < qvar1 movq (qvar1) , %mm0 movq (qvar2) , %mm1 pcmpgtb %mm1 ,%mm0 movl $1 xorl %ebx int $0x80 # ffff:ffff:ffff:ffff # ffff:0010:ff00:ff01 # ffff:0000:ff00:ff00 ,%eax ,%ebx # 0000:ffff:00ff:00ff # n.b. il confronto va da destra a sinistra ! # ffff:ffff:ffff:ffff # ffff:0010:ff00:ff01 # ffff:ffff:00ff:00ff # mm1 > mm0 # inverti gli operandi # ffff:ffff:ffff:ffff # ffff:0010:ff00:ff01 # 0000:0000:0000:0000 # mm1 < mm0 Shift Qui viene fatto un esempio di come vengono modificati i registri con l'utilizzo delle istruzioni di scorrimento. Il primo parametro puo' essere un registro oppure un valore che indica di quanto deve shiftare la posizione. Anche come (SAR) qui vengono prese in considerazione le rotazioni aritmetiche preservando il bit del segno, come potete vedere dall'esempio. .data qvar1: .quad 0x8002800280028002 .text _start: .globl _start # shift left (word *= 2 ) movq (qvar1) , %mm0 psllw $1 , %mm0 # 0002:0002:0002:0002 # 0004:0004:0004:0004 # aritmetic shift right (-word /= 2) movq (qvar1) , %mm0 psraw $1 , %mm0 movl $1 xorl %ebx int $0x80 ,%eax ,%ebx # 8002:8002:8002:8002 # c001:c001:c001:c001 Le istruzioni SSE del Pentium Il Pentium III introduce il set di istruzioni SSE (Streaming SIMD Extension), composto da 70 nuove istruzioni. Ben 50 di queste nuove istruzioni operano in modalità SIMD su numeri in virgola mobile a singola precisione. Il loro compito è accelerare alcune operazioni particolarmente utilizzate nel campo della grafica tridimensionale e dell'elaborazione audio. Si tratta in genere di calcoli in cui la stessa operazione deve essere ripetuta più volte su dati differenti. Per le operazioni in virgola mobile su questi registri sono presenti nuove istruzioni di : – – – – – – – addizione ; sottrazione ; moltiplicazione; divisione ; radice quadrata ; approssimazione rapida del reciproco ; approssimazione della radice quadrata inversa. Altre 12 istruzioni estendono il set di comandi MMX originario e operano su interi accelerando calcoli specifici della riproduzione video. Sono state introdotte nuove istruzioni per gli interi (in particolare sommatoria di differenze assolute e calcolo della media) così da velocizzare le funzioni di compensazione e stima di moto, caratteristiche della codifica MPEG- 2. Infine altre 8 istruzioni consentono, al software che ne faccia uso, di controllare esplicitamente il flusso dei dati dalla memoria centrale al processore attraverso la cache. In questo modo è possibile ad esempio evitare che la memoria cache si liberi di dati che dovranno poi essere riutilizzati, risparmiando preziosi cicli di clock. Accompagnano queste istruzioni otto nuovi registri da 128 bit (XMMx) : xmm0 128 bit xmm1 128 bit ... xmm7 128 bit Ciascuno capace di contenere quattro valori in virgola mobile a singola precisione. Su questi nuovi registri le istruzioni SSE possono operare in modalità SIMD (Single Instructions Multiple Data: la stessa istruzione applicata a più dati contemporaneamente). Intel affianca così le proprie istruzioni SIMD applicate ai valori in virgola mobile a quelle 3DNow! di AMD e a quella AltiVec del PowerPC (le precedenti istruzioni MMX sono infatti SIMD ma operano solo su valori interi). Sia AMD che Intel trattano i numeri in virgola mobile in una modalità grazie alla quale un risultato di underflow (un errore che si verifica quando il numero ottenuto è troppo piccolo per essere conservato nel registro) viene automaticamente posto a zero, evitando così la generazione di un errore nel programma. Questa modalità è particolarmente utile nelle applicazioni 3D, per le quali non è essenziale la precisione assoluta del risultato. Le SSE possono però anche operare in maniera convenzionale. In più c'è l’introduzione di una nuova modalità operativa, cosa che non accadeva dai tempi del 386, grazie alla quale è possibile eseguire le istruzioni SIMD in virgola mobile contemporaneamente a quelle classiche a doppia precisione o a quelle MMX, cosa invece impossibile nell'architettura 3DNow! di AMD, visto che i registri sono condivisi. Aritmetiche ADDPS, ADDSS, SUBPS, SUBSS, MULPS, MULSS, DIVPS, DIVSS, SQRTPS, SQRTSS, MAXPS, MAXSS, MINPS, MINSS Logiche ANDPS, ANDNPS, ORPS, XORPS Confronto CMPPS, CMPSS, COMISS, UCOMISS Mescolamento (Shuffle) SHUFPS, UNPCHKPS, UNPCKLPS Conversione CVTPI2PS, CVTPI2SS, CVTPS2PI, CVTSS2SI Spostamento MOVAPS, MOVUPS, MOVHPS, MOVLPS, MOVMSKPS, MOVSS Gestione dello Stato LDMXCSR, FXSAVE, STMXSCR, FXSTOR, MASKMOVQ, MOVNTQ, MOVNTPS, Controllo della Cache PREFETCH, SFENCE SIMD su interi (MMX esteso) PEXTRW, PINSRW, PMAXUB, PMAXSW, PMINUB, PMINSW, PMOVMSKB, PMULHUW, PSHUFW Fortunatamente per quanto avveniva diversamente dal set di istruzioni MMX e FPU, è possibile mischiare le operazionie MMX SSE (SSE2). Verifica presenza set istruzioni Prima di poter utilizzare il set di istruzioni SSE occorre verificare che la macchina supporta tali operazioni, come le istruzioni mmx utilizziamo l'istruzione CPUID che ci fornisce informazioni sul processore, e testare il bit 25. Esempio : MOVL $1, %EAX CPUID # RICHIESTA DEL FLAG CPUID # (0FH, 0A2H) ISTRUZIONE TEST $0x2000000H,%EDX # TEST BIT 25 (SSE) JNZ # OK SSE_DISPONIBILE RET SSE_DISPONIBILE: # ... Istruzioni per lo spostamento dei dati .data fp1: .float 1.1 .float 2.2 .float 3.3 .float 4.4 _start: .text .globl _start # tralascio la parte che identifica la presenza di sse movups fp1 movaps %xmm0 , %xmm0 , %xmm1 # (1234) muovi 4 valori fp in xmm0 # (1234) copia in xmm1 movhps fp1 movlps fp1 , %xmm3 , %xmm3 # (0012) muovi 2 valori in xmm3 (high) # (1212) muovi 2 valori in xmm3 (low) movlhps %xmm0 , %xmm4 # (0012) muovi 2 valori da (low) a (high) movhlps %xmm0 , %xmm4 # (3412) muovi 2 valori da (high) a (low) movss fp1 , %xmm5 movss movups %xmm0 , %xmm6 fpn , %xmm0 # (1000) muovi un valore in xmm5 (lowest) # (1000) muovi un valore in xmm6 (lowest) # (....) muovi 2 -ve, 2 +ve valori in xmm0 movmskps %xmm0, %eax # (0x09) prendi tutti i bit di segno da xmm0 a eax movl $1,%eax xorl %ebx,%ebx int $0x80 Istruzioni aritmetiche .data fp1: .float .float .float .float 1.1 2.2 3.3 4.4 .float .float .float .float 10.11 20.22 30.33 40.44 fp2: .text .globl _start _start: movups fp1 ,%xmm0 # move 1st tester fp values into xmm0 movaps %xmm0,%xmm2 # copying to xmm2 movups fp2 ,%xmm0 # move 2nd tester fp values into xmm1 movaps addps movaps subps %xmm1,%xmm3 %xmm1,%xmm0 %xmm2,%xmm0 %xmm1,%xmm0 # # # # copying to xmm3 add all fp values result in xmm0 restore value in xmm0 subtract all fp values result in xmm0 movaps %xmm2,%xmm0 # restore value in xmm0 addss %xmm1,%xmm0 # add lowest fp value result in xmm0 subss %xmm1,%xmm0 # subtract lowest fp value result in xmm0 movaps %xmm2,%xmm0 # restore value in xmm0 mulps %xmm1,%xmm0 # multiply all fp values result in xmm0 movaps %xmm2,%xmm0 # restore value in xmm0 mulss %xmm1,%xmm0 # multiply lowest fp value result in xmm0 movaps %xmm2,%xmm0 # restore value in xmm0 divps %xmm1,%xmm0 # divide all fp values result in xmm0 movaps %xmm2,%xmm0 # restore value in xmm0 divss %xmm1,%xmm0 # divide lowest fp value result in xmm0 movaps %xmm2,%xmm0 # restore value in xmm0 rcpps %xmm1,%xmm0 # get reciprocals of all # fp values result in xmm0 movaps %xmm2,%xmm0 # restore value in xmm0 rcpss %xmm1,%xmm0 # get reciprocal of lowest # fp value result in xmm0 movaps %xmm2,%xmm0 sqrtps %xmm1,%xmm0 # restore value in xmm0 # get square roots of all # fp values result in xmm0 movaps %xmm2,%xmm0 sqrtss %xmm1,%xmm0 # restore value in xmm0 # get square root of lowest # fp value result in xmm0 movaps %xmm2,%xmm0 # restore value in xmm0 rsqrtps %xmm1,%xmm0 # get reciprocals of square roots of # all fp values result in xmm0 movaps %xmm2,%xmm0 # restore value in xmm0 rsqrtss %xmm1,%xmm0 # get square root of lowest # fp value result in xmm0 movaps %xmm2,%xmm0 maxps %xmm1,%xmm0 # restore value in xmm0 # get numerically greater # fp values result in xmm0 movaps %xmm2,%xmm0 maxss %xmm1,%xmm0 # restore value in xmm0 # get numerically greater of low # fp values result in xmm0 movaps %xmm2,%xmm0 minps %xmm1,%xmm0 # restore value in xmm0 # get numerically smaller # fp values result in xmm0 movaps %xmm2,%xmm0 minss %xmm1,%xmm0 # restore value in xmm0 # get numerically smaller of low # fp values result in xmm0 movl $1 xorl %ebx int $0x80 ,%eax ,%ebx Set di istruzioni logiche : .data fp1: fp2: .float .float .float .float 1.1 2.2 3.3 4.4 .float .float .float .float 10.11 20.22 30.33 40.44 .text .globl _start _start: movups movaps movups movaps andps fp1 ,%xmm0 %xmm0,%xmm2 fp2 ,%xmm1 %xmm1,%xmm3 %xmm1,%xmm0 # # # # # move 1st tester fp values into xmm0 copying to xmm2 move 2nd tester fp values into xmm1 copying to xmm3 perform and on all fp values result in xmm0 movaps %xmm2,%xmm0 # restore value in xmm0 andnps %xmm1,%xmm0 # perform and not on all # fp values result in xmm0 movaps %xmm2,%xmm0 # restore value in xmm0 orps %xmm1,%xmm0 # perform or on all fp values result in xmm0 movaps %xmm2,%xmm0 # restore value in xmm0 xorps %xmm1,%xmm0 # perform xor on all fp values result in xmm0 movl $1,%eax xorl %ebx,%ebx int $0x80 Set istruzioni di confronto .data fp1: .single 1.2 .single 2.3 .single 3.4 .single 4.5 fp2: .single 10.21 .single 20.32 .single 30.43 .single 40.54 fpn: .double 0.0 .double 0.0 _start: .text .globl _start movups movups movss movaps (fp1),%xmm0 (fp2),%xmm1 %xmm1,%xmm0 %xmm0,%xmm2 # # # # move 1st tester fp values into xmm0 move 2nd tester fp values into xmm1 make lowest of xmm0 and xmm1 the same copying to xmm2 movaps cmpeqps %xmm1,%xmm3 %xmm1,%xmm0 # copying to xmm3 # cmpeqps see whether equal, result in xmm0 movaps cmpltps movaps cmpleps %xmm2,%xmm0 %xmm1,%xmm0 %xmm2,%xmm0 %xmm1,%xmm0 # # # # movaps cmpunordps restore cmpltps restore cmpleps original value to xmm0 see whether less than, result in xmm0 original value to xmm0 see whether less than or equal, result in xmm0 %xmm2,%xmm0 # restore original value to xmm0 %xmm1,%xmm0 # cmpunordps see unordered, result in xmm0 movaps cmpneqps %xmm2,%xmm0 %xmm1,%xmm0 # restore original value to xmm0 # cmpneqps see whether not equal, result in xmm0 movaps cmpnltps %xmm2,%xmm0 %xmm1,%xmm0 # restore original value to xmm0 # cmpnltps see whether not less than, result in xmm0 movaps cmpnleps %xmm2,%xmm0 %xmm1,%xmm0 # restore original value to xmm0 # cmpnleps see whether not less than or equal, # result in xmm0 movaps cmpordps %xmm2,%xmm0 %xmm1,%xmm0 # restore original value to xmm0 # cmpordps see whether ordered, result in xmm0 # compare instructions working on lowest only movaps cmpeqps %xmm2,%xmm0 %xmm1,%xmm0 # restore original value to xmm0 # cmpeqps see whether equal, result in xmm0 movaps cmpltps %xmm2,%xmm0 %xmm1,%xmm0 # restore original value to xmm0 # cmpltps see whether less than, result in xmm0 movaps cmpleps %xmm2,%xmm0 %xmm1,%xmm0 # restore original value to xmm0 # cmpleps see whether less than or equal, # result in xmm0 movaps cmpunordps xmm0 %xmm2,%xmm0 %xmm1,%xmm0 # restore original value to xmm0 # cmpunordps see unordered, result in xmm0 movaps cmpneqps %xmm2,%xmm0 %xmm1,%xmm0 # restore original value to xmm0 # cmpneqps see whether not equal, result in xmm0 movaps cmpnltps %xmm2,%xmm0 %xmm1,%xmm0 # restore original value to xmm0 # cmpnltps see whether not less than, result in movaps cmpnleps %xmm2,%xmm0 %xmm1,%xmm0 # restore original value to xmm0 # cmpnleps see whether not less than or equal, # result in xmm0 movaps cmpordps %xmm2,%xmm0 %xmm1,%xmm0 # restore original value to xmm0 # cmpordps see whether ordered, result in xmm0 # compare and give result in eflags movaps %xmm0,%xmm2 comiss %xmm0,%xmm1 ucomiss %xmm0,%xmm1 # restore original value to xmm0 # look at lowest only result in eflags # (unordered compare) movups (fpn),%xmm0 comiss %xmm0,%xmm1 ucomiss %xmm0,%xmm1 # move two -ve, two +ve values into xmm0 # look at lowest only - result in eflags # (unordered compare) movl $1,%eax xorl %ebx,%ebx int $0x80 Istruzioni di shuffle .data fp1: .single .single .single .single 1.2 2.3 3.4 4.5 fp2: .single .single .single .single 10.21 20.32 30.43 40.54 .text .globl _start _start: movups movaps movups movaps fp1 %xmm0 fp2 %xmm1 movaps unpckhps movaps unpcklps ,%xmm0 ,%xmm2 ,%xmm1 ,%xmm3 %xmm2,%xmm0 %xmm1,%xmm0 %xmm2,%xmm0 %xmm0,%xmm0 movl $1,%eax xorl %ebx,%ebx int $0x80 # # # # move 1st tester fp values into xmm0 copying to xmm2 move 2nd tester fp values into xmm1 copying to xmm3 # # # # restore original value to xmm0 unpack (high) and put into destination restore original value to xmm0 unpack (low) and put into destination Istruzioni di conversione Queste istruzioni convertono double word di interi in singola precisione (32-bit). .data dintero: .quad 0x1234567890abcdef .text .globl _start _start: cvtpi2ps dintero,%xmm0 cvtsi2ss dintero,%xmm1 cvtps2pi %xmm0 ,%mm0 cvttps2pi %xmm0 cvtss2si %xmm1 ,%mm1 ,%eax cvttss2si %xmm1 ,%edx movl $1,%eax xorl %ebx,%ebx int $0x80 # convert 23 and 24 to # single-precision fp values # convert 23 only to single-precision # fp value # convert 23 and 24 back again # from xmm0 into mm0 # same as above but with truncation # convert 23 back again # from xmm1 into eax # same as above but with truncation XMM e istruzioni operanti sugli interi Ci sono istruzioni sse2 operanti sugli interi che utilizzano i registri xmm a 128 bit. Esse sono state introdotte con il pentium 4 ed i processori xeon. Per convenienza vengono suddivise in due parti : La prima parte illustra le estensioni operanti sulle istruzioni MMX sse. La seconda parte illustra altre sse2 istruzioni operanti sugli interi per i registri XMMx. Prima di utilizzare tali istruzioni nel tuo codice occorre identificare se se esse sono disponibili sul processore. Questo test come gli altri viene effettuato chiamando l'istruzione CPUID con eax=1 e controllando il bit 26 in edx. Omportante dichiarare .dati con un allineamento corretto, in questo caso .align 16 che allinea i dati. in questo caso l'istruzione MOVDQA (move aligned double quadword sarà molto veloce) delle meno efficiente movDQU (move unaligned double quadword) che caricherà ugualmente i dati che siano allineati o meno. .data .align 16 v1: .quad 0x01234567890abcde .quad 0x0011223344556677 v2: .quad 0x01234567890abcde .quad 0x0011223344556677 se noi utilizzassimo l'istruzione per caricare i dati senza un corretto allineamento, il progrmamma si bloccherebbe ritornando il solito errore di “segmentation fault”. Come potete vedere dall'esempio i dati vengono caricati prima non allineati e poi viene utilizzata la migliore istruzione movdqa. Questo è il test per verificare la presenza del set di istruzioni : _start: .globl _start MOVl $1 ,%EAX CPUID TEST $0x4000000 ,%edx JNZ sse2_ok jmp fine sse2_ok: # # # richiedi i flag CPUID 0Fh, 0A2h CPUID istruzione test bit 26 (SSE2) Istruzioni sse2 (128bit) operanti sugli interi .align 16 # importante !!! .data v1: v2: .quad 0x01234567890abcde .quad 0x0011223344556677 .quad 0x01234567890abcde .quad 0x0011223344556677 .text _start: .globl _start movl $1 ,%eax cpuid test $0x4000000 ,%edx jnz sse2_ok # richiedi i flag cpuid # 0fh, 0a2h cpuid istruzione # test bit 26 (sse2) jmp fine sse2_ok: # provate a non allineare i dati con .align 16 # es. sostituire la prima riga con # movdqa v1 , %xmm0 # give 1st tester values to xmm0 # sicuramente incapperete in un “segmentation fault error” ! movdqu movdqa movdqu movdqa pavgb movdqa pavgw v1 , %xmm0, v2 , %xmm1, %xmm1, %xmm2, %xmm1, %xmm0 %xmm2 %xmm1 %xmm3 %xmm0 %xmm0 %xmm0 # # # # # # # give 1st tester values copying to xmm2 give 2nd tester values copying to xmm3 packed average-by-byte restore packed average-by-word to xmm0 to xmm1 result in xmm0 result in xmm0 # ********************* xmm extract to general purpose register pextrw pextrw pextrw pextrw $2,%xmm1,%eax $0,%xmm1,%edx $4,%xmm1,%esi $7,%xmm1,%edi # # # # extract extract extract extract 3rd 1st 5th 8th word word word word of of of of xmm0 xmm0 xmm0 xmm0 (low) to eax (low) to edx (high) to esi (high) to edi # ********************* xmm insert from general purpose register pinsrw pinsrw pinsrw pinsrw $0,%eax,%xmm0 $2,%edx,%xmm0 $4,%esi,%xmm0 $7,%edi,%xmm0 # # # # insert insert insert insert eax edx esi edi to to to to 1st 3rd 5th 8th word word word word of of of of xmm0 xmm0 xmm0 xmm0 (low) (low) (high) (high) # ********************* report xmm byte maximum movdqa %xmm2,%xmm3 pmaxub %xmm0,%xmm2 # report greater-by-byte in xmm3 # ********************* report xmm byte minimum movdqa %xmm2,%xmm3 pminub %xmm0,%xmm3 # report lesser-by-byte in xmm3 # ********************* compute sum of absolute differences movdqa %xmm2,%xmm3 psadbw %xmm0,%xmm3 # sum of absolute differences in xmm3 #********************* report xmm word maximum movdqa %xmm2,%xmm3 pmaxsw %xmm0,%xmm3 # report greater-by-word in xmm3 # ********************* report xmm word minimum movdqa %xmm2,%xmm3 pminsw %xmm0,%xmm3 # report lesser-by-word in xmm3 # multiply packed unsigned word integers high word result only movdqa %xmm3,%xmm0 pmulhuw %xmm3,%xmm0 # xmm3 * xmm0, high word result in xmm0 # create byte mask from most significant bits pmovmskb %xmm0,%eax fine: movl $1,%eax xorl %ebx,%ebx int $0x80 Other SSE2 integer instructions for the XMM registers .data .align 16 v1: .quad 0x01234567890abcde .quad 0x0011223344556677 v2: .quad 0x01234567890abcde .quad 0x0011223344556677 .text _start: .globl _start movl $1 ,%eax cpuid test $0x4000000 ,%edx jnz sse2_ok # richiedi i flag cpuid # 0fh, 0a2h cpuid istruzione # test bit 26 (sse2) jmp fine sse2_ok: movdqu movdqa movdqu movdqa paddq movdqa psubq movdqa v1 , %xmm0, v2 , %xmm1, %xmm1, %xmm2, %xmm1, %xmm2, %xmm0 %xmm2 %xmm1 %xmm3 %xmm0 %xmm0 %xmm0 %xmm0 pslldq $5 ,%xmm0 psrldq $5 ,%xmm0 movdqa %xmm2,%xmm0 # # # # # give 1st tester values to xmm0 copying to xmm2 give 2nd tester values to xmm1 copying to xmm3 packed quadword add # packed quadword subtract # shift double quadword left logical (5 bytes) # shift double quadword right logical (5 bytes) punpckhqdq %xmm1,%xmm0 # unpack high quadwords punpcklqdq %xmm1,%xmm0 # unpack low quadwords movdqa pmuludq movdqa pshufd %xmm2,%xmm0 %xmm1,%xmm0 # multiply packed unsigned dword integers %xmm2,%xmm0 $0x33,%xmm1,%xmm0 # shuffle packed doubleword integers movdqa %xmm2,%xmm0 pshuflw $0x33,%xmm1,%xmm0 # shuffle packed low words pshufhw $0x33,%xmm1,%xmm0 # shuffle packed high words movdq2q %xmm1, %mm0 movq2dq %mm0 , %xmm6 fine: movl $1,%eax xorl %ebx,%ebx int $0x80 # move qword integer from xmm to mmx # move qword integer from mmx to xmm "Use of mnemonics" demonstrations XMM SSE2 floating point instructions Prima abbiamo visto le istruzioni sse2 che agivano direttamente sugli interi ora analizziamo altre istruzioni che tramite gli stessi registri trattano i numeri in virgola mobile. Generalmente queste istruzioni sono molto simili alle istruzioni sse che operano sulla virgola mobile, eccetto che per le dimensioni del dato sulle quali lavorano. Anche in questo caso occorre fare un testo sul bit 26 di edx per vedere se viene supportato ilset di istruzioni, tramite l'usuale cpuid. .data .align 16 fp1: .double 1.1 .double 2.2 fp2: .double 10.66 .double 20.66 fp3: .double -3.4 .double +1.2 anche in questo caso i dati devono essere allineati correttamente .align 16. (16-bit buondary). E' comunque possibile come prima caricare inmemoria i dati anche se non sono allineati ma il il secondo metodo risulta molto più veloce. (movdqu movdqa) il set di istruzioni sse2 viene cosi suddiviso : - Data movement instructions ; Arithmetic instructions ; Logical instructions ; Comparison instructions ; Shuffle and unpack instructions ; Conversion instructions. SSE2 Data movement instructions Queste semplici istruioni mostrano come muovere i dati dalla memoria e tra i registri, in modo allineato o non. MOVMSKPD può essere utilizzato per ottenere i risultati dopo un'istruzione di confronto. .data .align 16 fp1: .double 1.1 .double 2.2 fp2: .double 10.66 .double 20.66 fp3: .double -3.4 .double +1.2 .text _start: .globl _start movl $1 cpuid test $0x4000000 jnz sse2_ok ,%eax # richiedi i flag cpuid # 0fh, 0a2h cpuid istruzione # test bit 26 (sse2) ,%edx movupd fp1 ,%xmm0 movapd movsd movlpd movhpd movupd movmskpd %xmm0 fp2 fp2 fp2 fp3 %xmm0 jmp fine sse2_ok: fine: movl $1,%eax xorl %ebx,%ebx int $0x80 ,%xmm7 ,%xmm2 ,%xmm3 ,%xmm4 ,%xmm0 ,%eax # move two double precision # fp values into xmm0 # # # # # # copying to xmm7 move fp value to xmm1 low only this seems to be the same but this moves the high value move two new values, one is negative get both sign bits in xmm0 into eax SSE2 Arithmetic instrunctions .data .align 16 fp1: .double 1.1 .double 2.2 fp2: .double 10.66 .double 20.66 fp3: .double -3.4 .double +1.2 .text _start: .globl _start movl $1 ,%eax # richiedi i flag cpuid cpuid # 0fh, 0a2h cpuid istruzione test $0x4000000 ,%edx # test bit 26 (sse2) jnz sse2_ok movupd movapd movupd movapd addpd movapd subpd fp1 ,%xmm0 %xmm0,%xmm2 fp2 ,%xmm1 %xmm1,%xmm3 %xmm1,%xmm0 %xmm2,%xmm0 %xmm1,%xmm0 # # # # # # # move two double precision fp values into xmm0 copying to xmm2 move 2nd tester fp values into xmm1 copying to xmm3 add both fp values result in xmm0 restore value in xmm0 subtract both fp values result in xmm0 movapd %xmm2,%xmm0 # restore value in xmm0 addsd %xmm1,%xmm2 # add low fp value result in xmm0 subsd %xmm1,%xmm0 # subtract low fp value result in xmm0 movapd %xmm2,%xmm0 # restore value in xmm0 mulpd %xmm1,%xmm0 # multiply both fp values result in xmm0 movapd %xmm2,%xmm0 # restore value in xmm0 mulsd %xmm1,%xmm0 # multiply low fp value result in xmm0 movapd %xmm2,%xmm0 # restore value in xmm0 divpd %xmm1,%xmm0 # divide both fp values result in xmm0 movapd %xmm2,%xmm0 # restore value in xmm0 divsd %xmm1,%xmm0 # divide low fp value result in xmm0 movapd %xmm2,%xmm0 # restore value in xmm0 sqrtpd %xmm1,%xmm0 # get square roots of both fp values result in xmm0 movapd %xmm2,%xmm0 # restore value in xmm0 sqrtsd %xmm1,%xmm0 # get square root of low fp value result in xmm0 movapd %xmm2,%xmm0 # restore value in xmm0 maxpd %xmm1,%xmm0 # get numerically greater fp values result in xmm0 xmm0 movapd %xmm2,%xmm0 # restore value in xmm0 maxsd %xmm1,%xmm0 # get numerically greater of low fp values result in movapd %xmm2,%xmm0 # restore value in xmm0 minpd %xmm1,%xmm0 # get numerically smaller fp values result in xmm0 movapd %xmm2,%xmm0 # restore value in xmm0 minsd %xmm1,%xmm0 # get numerically smaller of low fp values result in xmm0 jmp fine sse2_ok: fine: movl $1 xorl %ebx int $0x80 ,%eax ,%ebx SSE2 Logical instructions .data .align 16 fp1: .double .double fp2: .double .double fp3: .double .double 1.1 2.2 10.66 20.66 -3.4 +1.2 .text .globl _start _start: movl $1 cpuid test $0x4000000 jnz sse2_ok jmp fine movupd movapd movupd movapd andpd movapd andnpd movapd orpd movapd xorpd ,%edx fp1 ,%xmm0 %xmm0,%xmm2 fp2 ,%xmm1 %xmm1,%xmm3 %xmm1,%xmm0 %xmm2,%xmm0 %xmm1,%xmm0 %xmm2,%xmm0 %xmm1,%xmm0 %xmm2,%xmm0 %xmm1,%xmm0 sse2_ok: fine: ,%eax movl $1,%eax xorl %ebx,%ebx int $0x80 # # # # # # # # # # # # # # richiedi i flag cpuid 0fh, 0a2h cpuid istruzione test bit 26 (sse2) move two double precision fp values into %xmm0 copying to %xmm2 move 2nd tester fp values into %xmm1 copying to xmm3 perform and on both fp values result in %xmm0 restore value in %xmm0 perform and not on both fp values result in %xmm0 restore value in %xmm0 perform or on both fp values result in %xmm0 restore value in %xmm0 perform xor on both fp values result in %xmm0 SSE2 Comparison instructions fp2: .double .double fp3: .double .double 10.66 20.66 -3.4 +1.2 .text .globl _start _start: MOVl $1 ,%EAX CPUID TEST $0x4000000 ,%edx JNZ sse2_ok jmp fine %xmm0 MOVUPD fp1 ,%xmm0 MOVAPD %xmm0,%xmm2 MOVUPD fp2 ,%xmm1 MOVAPD %xmm1,%xmm3 # richiedi i flag CPUID # 0Fh, 0A2h CPUID istruzione # test bit 26 (SSE2) #move two double precision fp values into #copying to %xmm2 #move 2nd tester fp values into %xmm1 #copying to %xmm3 # compare instructions working on both fp values %xmm0 CMPPD MOVAPD CMPPD MOVAPD CMPPD $0 ,%xmm1,%xmm0 %xmm2 ,%xmm0 $1 ,%xmm1,%xmm0 %xmm2 ,%xmm0 $2 ,%xmm1,%xmm0 MOVAPD CMPPD MOVAPD CMPPD %xmm2 ,%xmm0 $3 ,%xmm1,%xmm0 %xmm2 ,%xmm0 $4 ,%xmm1,%xmm0 MOVAPD %xmm2 ,%xmm0 CMPPD $5 ,%xmm5,%xmm0 MOVAPD %xmm2 ,%xmm0 CMPPD $6 ,%xmm1,%xmm0 MOVAPD %xmm2 ,%xmm0 CMPPD $7 ,%xmm1,%xmm0 #=CMPEQPD see whether equal, result in %xmm0 #restore original value to %xmm0 #=CMPLTPD see whether less than, result in %xmm0 #restore original value to %xmm0 #=CMPLEPD see whether less #than or equal, result in %xmm0 #restore original value to %xmm0 #=CMPUNORDPD see unordered, result in %xmm0 #restore original value to %xmm0 #=CMPNEQPD see whether not equal, result in #restore original value to %xmm0 #=CMPNLTPD see whether not less than, #result in %xmm0 #restore original value to %xmm0 #=CMPNLEPD see whether #not less than or equal, result in %xmm0 #restore original value to %xmm0 #=CMPORDPD see whether ordered, result in %xmm0 # compare instructions working on low value only %xmm0 MOVAPD CMPSD MOVAPD CMPSD MOVAPD CMPSD %xmm2 ,%xmm0 $0 ,%xmm1,%xmm0 %xmm2 ,%xmm0 $1 ,%xmm1,%xmm0 %xmm2 ,%xmm0 $2 ,%xmm2,%xmm0 MOVAPD CMPSD MOVAPD CMPSD %xmm2 ,%xmm0 $3 ,%xmm1,%xmm3 %xmm2 ,%xmm0 $4 ,%xmm1,%xmm0 MOVAPD %xmm2 ,%xmm0 CMPSD $5 ,%xmm1,%xmm0 MOVAPD %xmm2 ,%xmm0 CMPSD $6 ,%xmm1,%xmm0 MOVAPD %xmm2 ,%xmm0 CMPSD $7 ,%xmm1,%xmm0 #restore original value to %xmm0 #=CMPEQPD see whether equal, result in %xmm0 #restore original value to %xmm0 #=CMPLTPD see whether less than, result in %xmm0 #restore original value to %xmm0 #=CMPLEPD see whether #less than or equal, result in %xmm0 #restore original value to %xmm0 #=CMPUNORDPD see unordered, result in %xmm0 #restore original value to %xmm0 #=CMPNEQPD see whether not equal, result in #restore original value to %xmm0 #=CMPNLTPD see whether not less than, #result in %xmm0 #restore original value to %xmm0 #=CMPNLEPD see whether #not less than or equal, result in %xmm0 #restore original value to %xmm0 #=CMPORDPD see whether ordered, result in %xmm0 # compare and give result in eflags MOVAPD %xmm2,%xmm0 COMISD %xmm1,%xmm0 UCOMISD %xmm1,%xmm0 #restore original value to %xmm0 #look at lowest only result in eflags #(unordered compare) MOVUPD fp3,%xmm1 COMISD %xmm1,%xmm0 UCOMISD %xmm1,%xmm0 #move two -ve, two +ve values into %xmm1 #look at lowest only - result in eflags #(unordered compare) sse2_ok: fine: movl $1,%eax xorl %ebx,%ebx int $0x80 SSE2 Shuffle and unpack instructions .data .align 16 fp1: .double .double fp2: .double .double fp3: .double .double 1.1 2.2 10.66 20.66 -3.4 +1.2 .text .globl _start _start: MOVl $1 ,%EAX CPUID TEST $0x4000000 ,%edx JNZ sse2_ok jmp fine %xmm0 MOVUPD fp1, MOVAPD MOVUPD MOVAPD SHUFPD SHUFPD MOVAPD UNPCKHPD MOVAPD UNPCKLPD %xmm2 ,%xmm0 fp2 ,%xmm1 %xmm3 ,%xmm1 $3 ,%xmm0,%xmm1 $1 ,%xmm0,%xmm0 %xmm0 ,%xmm2 %xmm0 ,%xmm1 %xmm0 ,%xmm2 %xmm0 ,%xmm0 sse2_ok: fine: movl $1,%eax xorl %ebx,%ebx int $0x80 %xmm0 # richiedi i flag CPUID # 0Fh, 0A2h CPUID istruzione # test bit 26 (SSE2) # move two double precision fp values # # # # # # # # # copying to %xmm2 move 2nd tester fp values into %xmm1 copying to %xmm3 shuffle pack into destination swap the values in %xmm0 restore original value to %xmm0 unpack (high) and put into destination restore original value to %xmm0 unpack (low) and put into destination into SSE2 Conversion instructions .data .align 16 fp1: .double 1.1 .double 2.2 fp2: .double 10.66 .double 20.66 dint: .quad 0x0011223344556677 .quad 0xaabbccddeeff0011 .text .globl _start _start: MOVl $1 ,%EAX CPUID TEST $0x4000000 ,%edx JNZ sse2_ok jmp fine # # richiedi i flag CPUID # 0Fh, 0A2h CPUID istruzione # test bit 26 (SSE2) conversion between single and double-precision fp values CVTPS2PD fp1 ,%xmm0 CVTPD2PS %xmm6 ,%xmm0 CVTSS2SD fp1 CVTSD2SS %xmm7 ,%xmm1 ,%xmm1 #put single-precision fp #values into %xmm0 as double-precision #convert double precision to #single precision in %xmm7 #as CVTPS2PD but working with only one value #as CVTSS2SD but working with only one value # conversion between integers and double-precision fp values .. # open the MMX integer pane for these tests .. CVTPD2PI MM0 %xmm0 CVTTPD2PI %xmm0 CVTPI2PD dint ,%mm0 #convert fp values in %xmm0 to integers in ,%mm1 ,%xmm0 #same as above with truncation #convert 23 and 24 to double-precision #fp values #open the %xmm integer display and switch to dword display CVTPD2DQ %xmm7,%xmm0 CVTTPD2DQ CVTDQ2PD CVTSD2SI CVTTSD2SI CVTSI2SD %xmm7,%xmm0 %xmm3,%xmm7 %xmm0,%eax %xmm0,%edx %eax,%xmm4 # and convert 23 and 24 to dword integers # into %xmm7 (low) # same as above with truncation # and back into fp values in %xmm3 # take low fp value and convert as integer in EAX # same as above with truncation # and back again into %xmm4 (low) # conversion between single-precision and integers .. # watch these in %xmm integer display switched to dword display CVTPS2DQ fp1 ,%xmm0 CVTTPS2DQ fp1 ,%xmm1 # move 4 single-precision fp values to # dwords as integers # same as above with truncation # and watch this in the SSE fp pane .. CVTDQ2PS CVTDQ2PS %xmm6,%xmm0 %xmm7,%xmm1 sse2_ok: fine: movl $1,%eax xorl %ebx,%ebx int $0x80 # and convert back to 4 single-precision fp values CAPITOLO 15 Parola d'ordine : Ottimizzare ! Parola d'ordine : Ottimizzare ! Questo è uno dei capitoli più importanti del libro, in quanto si verranno delineati alcuni aspetti interni della gestione da parte della cpu delle istruzioni e dei dati; Oggi come oggi i compilatori ottimizzano notevolmente il codice tanto da prevalere in compattezza e velocità ad un programmatore in assembler alle prime armi. Tuttavia con opportuni accorgimenti è possibile strizzare il codice per renderlo più efficiente del codice generato dal compilatore, ed ottenere così una miglior padronanza del linguaggio assembly, anche se oggi viene utilizzato solo per programmare alcune routine, questo per il fatto della sua non portatibilità su altre piattaforme e della difficoltà della manuntezione del codice, oltretutto occorre diverso tempo per impratichirsi del linguaggio assembly ad eccezione di altri linguaggi di più alto livello che con il solito copia e incolla che dopo un paio di milioni di byte aprono una bella finestrella colorata. Comunque inziamo ! Regola numero uno : un codice piccolo solitamente è più performate ! Questa affermazione nella maggior parte dei casi è vera in quanto la cpu impiega meno cicli nel leggere e nel decodificare e eseguire l'istruzione; altre volte non è cosi! Occorre tener ben presente anche l'architettura della macchina e delle varie ottimizzazioni che sono state inserite, benchè l'unità di base sia il byte e questo mi va bene nella lettura in un 8086, ma su di un pentium con architettura a 32 bit la lettura del registro eax è più veloce, seppur di 4 byte. esempio : xorl %eax,%eax # eax = 0 movl $0 ,%eax# eax = 0 Queste due istruzioni producono lo stesso risultato tuttavia la seconda è più lunga un solo byte, la prima quindi è più veloce in quanto non deve caricare il valore immediato presente nella prima. In definitiva in questo caso un codice più piccolo effettivamente è più efficiente. La memoria Se non ricordo male, l'avevo già spiegato (questione di memoria). Vediamo ora un aspetto della gestione della memoria un poco più approfondito. La memoria fisica è un array lineare di byte. L'indirizzo della memoria fisica parte da zero fino al massimo indirizzamento consentito dai registri (cs:eip). Nei modelli flat non vengono più usati i segmenti. Nel caso del modello flat 4 giga byte. E' possibile aumentare questo limite tramite la memoria di swap, quella virtuale o altri sistemi. In definiva la memoria è composta da byte, il byte è l'unità più piccola indirizzabile da un elaboratore; Quando leggiamo word, dword e quad leggiamo una successione di byte. Dicevo la memoria è composta SOLO da byte, per poter leggere word, dword ecc. ogni architettura presenta soluzioni diverse. Per esempio nella famiglia x86, per quanto riguarda le word queste vengono memorizzate in ordine inverso prima il byte basso (l.o.) poi quello alto (h.o.), perciò una word utilizza 2 indirizzi di memoria. Così in modo analogo la stessa memorizzazione avvienne per le dword ecc. L'x86 (8086) ha un bus indirizzi di un byte; questo significa che la cpu può trasferire alla volta solo 8 bits. Quindi se vogliamo accedere a soli 4 bits occorre leggere tutto l'ottetto, ancora se i 4 bits sono tra due indirizzi dovrò leggere 2 byte. Anche se 8088 e 80188 possono manipolare più bits alla volta occorrerà comunque più operazioni di lettura. Chiaramente se ci troviamo di fronte ad un'architettura a 32 bit possiamo caricare contemporaneamente 4 byte (32bit) il che porta a notevoli vantaggi di velocità. In un bus indirizzi a 16 bits, è possibile leggere 2 byte alla volta ora vediamo come. Il processore organizza la memoria in due banchi (16 bit) : – – banco pari banco dispari : : d0 – d7 l.o. d8 – d15 h.o. Apparentemente se carichiamo una word non dovremo aver problemi, teoricamente possiamo leggere qualsiasi indirizzo e caricare il dato che ci serve; ma non è così ! Se noi leggiamo una word alla locazione 200, non si presenta nessun problema in quanto da d0-d7 viene trasferito il dato pari quindi il byte basso e da d8-d15 viene traferito il byte alto. Il problema è quando cerchiamo di caricare un indirizzo (dispari) per esempio 201, in qusto caso i byte verranno letti al contrario! fortunatamente la cpu riconoscre questo problema e autonomamente scambia i byte nel giusto ordine, quindi impiega più tempo. Questo processo è nascosto al programmatore, tuttavia per quanto possiamo accedere alla memoria a qualsiasi indirizzo è preferibile accedere alla lettura di indirizzi pari ! Se in un'architettura a 32 bit accedi ad un indirizzo dispari per la cpu impiega 3 cicli aggiuntivi per sistemare il tutto. banco banco banco banco pari dispari pari dispari : : : : d0 d8 d16 d24 – – – – d7 d15 d23 d31 l.o. h.o. l.o. h.o. 1) La cpu a 32 bit impiega una sola operazione se legge da una locazione pari. 2) la lettura è più veloce se l'indirizzo è divisibile per 4. D'ora in poi utilizzeremo la direttiva .align per allineare correttamente i dati. Perchè il Pentium è CICSC ? Ogni istruzione in assembly ha un equivalente di un numero in linguaggio macchina, questo numero non identifica solamente l'istruzione ma porta con se altre informazioni sui registri, sulla classe se deve scrivere o leggere dal un registro o dalla memoria tutto in un ottetto! Quindi per codificare tutto questo in così poco spazio l'architettura delle istruzioni è complessa, vediamo come. Una tipica istruzione in linguaggio macchina prende questa forma : CBA rr mmm xxx xx xxx L'istruzione di base è nel foamto “CBA ” e questa ha tre bit quindi al massimo può contenere 8 istruzioni, tuttavia i progettisti sono riusciti a codificare 27 classi di istruzione ! : I primi tre bits dell'ottetto quando la cpu la decodifica fa riferimento a questa tabella CBA : 000 = istruzione speciale ; 001 = or 010 = and 011 = cmp 100 = sub 101 = add 110 = mov (reg, mem / reg / imm ) 111 = mov (mem,reg ) gli altri 2 bits dell'ottetto corispondo ai seguenti valori : 00 = ax 01 = bx 10 = cx 11 = dx infine i restanti 3 bits fanno riferimento a questa tabella : 000 = ax 001 = bx 010 = cx 011 = dx 100 = (bx) 101 = (xxx+bx) 110 = (xxx) 111 = imm xxx = memoria imm = immediato quindi in teoria da qui è possibile già codificare alcune istruzioni : esempio : cba rr mmm istruzione 110 00 001 movw %bx 110 00 101 movw 2000(%bx) , %ax 0xc5 0x00 0x20 110 00 110 movw 1000(%bx) , %ax 0xc5 0x00 0x10 ,%ax opcode 0xc0 L'opcode 000 (CBA) permette di estendere il set di istruzioni prendendo in considerazione questa forma : cba ii mmm 000 xx xxx dove ii : 00 = zero operando instruction 01 = jmp instruction 10 = not 111 = illegal (reserved) e mmm : 000 = ax 001 = bx 010 = cx 011 = dx 100 = (bx) 101 = xxx(bx) 110 = (xxx) 111 = imm per quanto riguarda poi i salti : cba ii mmm 000 01 xxx dove xxx : 000 = je 001 = jne 010 = jb 011 = jbe 100 = ja 101 = jae 110 = jmp 111 = illegal In linea di massima questo è il procedimento per creare gli opcode anche se poi in concretamente è molto più complesso, tuttavia mi è sembrato giusto introdurre questa breve dissertazione, per capire di più su funzionamento della cpu, come vedrete tra poco. La cpu x86 non completa l'esecuzione in un ciclo singolo, questa esegue diversi passaggi per ogni singola istruzione. Quando per esempio incontra l'istruzione movw %ax,%bx esegue queste operazioni : carica un byte dalla memoria ; esegue l'update di ip ; decodifica l'istruzione ; ( se richiesto carica un nuovo byte e aggiorna ip) – calcola l'indirizzo ; – esegue l'operazione ; – memorizza il risultato . – – – Quindi a seconda di “cosa fa”, i cicli operativi possono variare, vale ancora la regola che meno cicli operativi deve compiere più veloce è l'istruzione. Riepilogo costruzione opcode MOV REG/MEM DA/A REG 100010DW MOD REG R/M IMM A REG/MEM 1100011W MOD 000 R/M DATA IMM A REG 1011W REG DATA DATA SE W = 1 MEM A REG 1010000W L.O. ADD H.O. ADD REG A DATA SE W = 1 MOD 0 REG R/M MEM /(SEG REG) 10001110 (SEG REG) A REG/MEM 10001100 MOD 0 REG R/M La famigla x86, cugini, nipoti e nonni condividono lo stesso patrimonio genetico, cioè le stesse istruzioni, gli stessi modi di indirizzamento ed eseguono le istruzioni allo stesso modo, allora qual'è la differenza ? la risposta sta nelle caratteristiche hardware della sua architettura e di vengono gestite le istruzioni : – – – – pre-fetch ; cache ; pipelines ; superscalar-design ; Codifica Istruzioni Vi è un formato generale per formare l'opcode ed è : Formato generale dell'istruzione Cod. Operativo 1^ar. x 76543210:76543210 Codice operativo 1|2 byte registro ModTTTr/m Indice :76543210:76543210 Modo indirizzo d32|16|8|x d32|16|8| :76543210:76543210 spiazzamento immediato Tutte le altre codifiche, sono sotto insiemi di questa. Le istruzioni possono essere costituite da 1 o 2 byte di codice operativo primario eventualmente da uno specificatore di indirizzo costruito dal byte mod r/m e dal byte dell'indice scalata, uno spiazzamento se necessario e un campo di dati immediati se necessario. Nell'ambito del codice operativo primario, possono essere definiti campi di codifica più piccoli. Questi campi variano in funzione della classe di operazione. I campi definiscono informazioni come la direzione dell'operazione, la dimensione degli spiazzamenti, la codifica dei registri e l'estensione del segno. Quasi tutte le istruzioni si riferiscono ad un operando in memoria hanno un byte di modo di indirizzamento che segue il byte del codice (o i byte) operativo primario. Questo byte, il byte mod r/m specifica il modo di indirizzamento che deve essere usato. Certe codifiche del byte mo r/m indicano un secondo byte di indirizzamento, il byte scala/indice/base, che specifica completamente il modo di indirizzamento. I modi di indirizzamento possono comprendere uno spiazzamento immediato succesivo al byte r/m o al byte dell'indice scalato. Se è presente uno spiazzamento le dimensioni possibili sono 8,16 e 32 bit. Se l'istruzione specifica un operando immediato, questo segue gli eventuali byte di spiazzamento. L'operando immediato è sempre l'ultimo campo dell'istruzione. Di seguito fornisco il significato dei sotto campi che compongono l'istruzione. Nome Descrizione Bit w d s reg mod r/m ss index base sreg2 sreg3 mm Specifica la dimensione dei dati byte,oppure 16,32 specifica la direzione dei dati 1 specifica se il campo dati immediato deve essere esteso con segno specificatore registro generale specificatore modo indirizzamento indirizzamento indice scalato (fattore scala) 2 registro generale da usare come indice registro generale da usare come base registro di segmento CS,SS,DS,ES specificatore registro segmento CS SS DS ES FS GS per le istruzioni condizionali (positivo/negativo) 1 1 3 2 3 2 2 3 4 Estensioni a 32 bit Con x386 il set di istruzioni viene esteso i due direzioni, le istruzioni possono operare tutte a 16 o a 32 bit, quindi è come se avessimo due tabelle dove pescare le istruzioni. Questo trucco si ottiene mediante il bit di default (D) nel descittore del segmento codice e mediante due prefisso uno per definire il set a 16 bit e l'altro per quaello a 32. L'8086 non utilizza descrittori di segmento per operare. Il fratello maggiore imposta per default nel tipo di descrittore relativo a un blocco il tipo di indirizzamento di default. Due prefissi quello dalla dimensione dell'operando e quello della dimensione dell'indirizzo effettivo permettono di forzare la selezione implicita della dimensione di operando ed indirizzo effetivo. Questi byte precedeno e ne modificano l'istruzione. Per esempio se l'istruzione sta lavorando a 32 bit per default ed anteponiamo il byte suffisso a 16 bit questa lavorerà come richiesto. Codifica della lunghezza dell'operando : campo w 0 1 dimensione 16 bit 8 bit 16 bit dimensione 32 bit 8 bit 32 bit Codifica del campo registro generale : Registro 000 001 010 011 100 101 dimensione 16 bit ax cx dx bx sp bp,si,di dimensione 32 bit eax ecx edx ebx esp ebp,esi,edi Codifica Campo sreg3 : Campo sreg3 a 3 bit registro 000 001 010 011 100 101 110 110 es cs ss ds fs gs (da non usare) (da non usare) Codifica del modo di indirizzamento a 32 bit con byte mod r/m mod r/m 00 000 00 001 00 010 00 011 00 110 00 111 indirizzo effettivo ds:(eax) ds:(ecx) ds:(edx) ds:(ebx) ds:(esi) ds:(esi) 01 000 01 001 01 010 01 011 ds:(eax+d8) ds:(ecx+d8) ds:(edx+d8) ds:(ebx+d8) 10 000 10 001 10 010 10 011 ds:(eax+d32) ds:(ecx+d32) ds:(edx+d32) ds:(ebx+d32) Ho riportato solo alcuni esempi a titolo informativo, la trattazione risulta essere molto più estesa e complicata. Prefetch Con questa tecnica, si intente la capacità della macchina, di leggere oltre all'istruzione da eseguire anche altre sucessive ad essa, incorporandole in un piccolo buffer FIFO (first in first out). Questo avviene per ovviare il più possibile i colli di bottiglia per quanto riguarda le ottimizzazione della memoria. Di fatto leggere dalla memoria è molto lento rispetto alla velocità del processore. Cache Dal .486 è stato introdotta una memoria interna al computer molto veloce, per ovviare i tempi di attesa dalla lettura della ram. Partendo dal presuttposto che l'esecuzione di un programma sia sequenziale, si carica in memoria cache blocchi contigui di ram, (32 byte) quanto la memoria cache è ampia; ancora mediante opportuni algoritmi si mantiene in memoria quella che è la parte di codice che più viene utilizzata per non continuare a perdere tempo a caricarla dalla memoria. Vengono opportunamente gestiti anche le condizioni di salto. Pipeline E' un architettura che suddivide l'esecuzione delle istruzioni in parallelo per raddoppiare la velocità di computazione delle stesse. Nel pentium abbiamo due differenti pipeline la U e la V : – – U questa pipeline è in grado solo di eseguire istruzioni intere ; V è in grado di eseguire solo istruzioni semplici ; Tramite le due pipeline, quando possibile il pentium accoppia la loro esecuzione e quindi li esegue contemporaneamente. Perciò superscalare significa quando un processore è dotato di due o più pipeline in parallelo. Non porterò altri esempi sulla codifica delle istruzioni dall'assembly al linguaggio macchina, in quanto ho già esposto il concetto fondamentale relativi a quanto tempo impiega in termini di cicli per decodificare un' istruzione che varia al variare dei parametri da qui si possono ricavare delle semplice regole di ottimizzazione : – – lavorare sui registri è molto più veloce che non lavorare sulla memoria ; leggere da locazioni pari aumenta la velocità di lettura dalla memoria ; per quanto riguara gli indirizzamenti vale la stessa regola, tanto più è complesso la decodifica dell'indirizzo maggiore sarà il numero dei cicli che occorrerà per completare l'istruzione, in questa tabella metterò in ordine dal più veloce al più lento i vari indirizzamenti : – – – – – indirizzamento mediante registri indirizzamento indiretto modo immediato indirizzamento puntatore base indirizzamento indicizzato ; movl ; movl %eax ; movl (%eax) ; movl 0x11223344 ; movl 4(%ebp) array(%ebx,%ecx,4) altri esempi riguardanti le letture / scritture posso essere : movl movl movl movl $1000,%eax $1000,%ebx $1000,%ecx $1000,%edx è meno veloce di : movl movl movl movl $1000,%eax %eax ,%ebx %eax ,%ecx %eax ,%edx ,(%ebx) , %ebx , %eax , %eax , %eax Ottimizzare gli array in riferimento a quello descritto prima, semplificando il codice dovremo riuscire ad ottenere un incremento di prestazioni. al = ArrayByte[ i ] ; ArrayByte[0..65535] movw (i) , %ebx movb ArrayByte( %ebx ) , %al movw movl addl movb (i) $ArrayByte %ebx (%ebx) , , , , %eax %ebx %eax %eax questo secondo metodo anche se in apparenza piu' lungo risulta più ottimizzato e più veloce; ora la regola 1 non vale più vediamo! un esempio : .data .equ DIM,100000 i: .long 0 .bss .comm ArrayByte,DIM # cento mila byte .text .globl _start _start: #............... al = Arrabyte[i] movl $DIM,%ecx ciclo1: movl (i) ,%ebx # 1 ciclo movb ArrayByte(%ebx),%al # 1 ciclo incl (i) # 3 cicli loopnz ciclo1 # -------- 5 cicli #............... al = Arrabyte[i] movl $DIM , xorl %ebx , movl $ArrayByte, addl %eax , ciclo2: movb (%ebx),%al incl %ebx loopnz ciclo2 movl $1 ,%eax xorl %ebx,%ebx int $0x80 %ecx %ebx %eax %ebx # 1 ciclo # 1 ciclo # -------- 2 cicli Questo è un secondo esempio di come ottimizzare un ciclo, anticipo che il secondo metodo oltre ad essere più compatto e veloce sfrutta anche le pipeline accoppiate del pentium per raddoppiare le prestazioni, ma vedremo questo in dettaglio, più avanti : .data .equ DIM,100000 i: .long 0 .bss .comm ArrayDouble,DIM*8 # cento mila double .text .globl _start _start: #............... al = ArrayDouble[i] movl $DIM,%ecx movl $ArrayDouble,%ebx ciclo1: movl (i) ,%edi # 1 ciclo movl 0(%ebx,%edi,8) ,%eax # 1 ciclo movl 4(%ebx,%edi,8) ,%eax # 1 ciclo incl (i) # 3 cicli loopnz ciclo1 #---------- 7 cicli #............... al = ArrayDouble[i] movl $DIM ,%ecx xorl %ebx ,%ebx movl $ArrayDouble ,%eax addl %eax ,%ebx ciclo2: movl (%ebx) ,%eax # 1 ciclo addl $4 ,%ebx # 1 cicli movl (%ebx) ,%eax # 1 ciclo addl $4 ,%ebx # 1 cicli loopnz ciclo2 #---------- 4 cicli movl $1,%eax xorl %ebx,%ebx int $0x80 Calcolo indici array multidimensionali al = ArrayWord [r][c] ; ArrayWord[4][4] movl $ArrayWord movl r shl $3 addl c ,%ebx ,%eax ,%eax ,%eax # *1 # *4 addl %eax,%ebx Con questo esempio abbiamo eliminato il tempo in piu di MUL (10-11 cicli) ed utilzzato il registro %eax che è più ottimizzato. (shl # ciclo 1 U) Nel limite del possibile occorre cercare di utilizzare il meno possibile l'istruzione MUL , in quanto è molto lenta. Anziche dividere (DIV) è meglio moltiplicare per 0,xx . esempio : a=a/2 ; div più lento di MUL ; in questo caso meglio usare shr 1 a = a * 0.5 ; meglio la moltiplicazione è più veloce della divisione Array a 3 dimensioni Array[2][3][4] # indice = ((j*3)+k)*4+L)*2 movl j movl $3 mul %ebx addl k addl %eax addl %eax addl L addl %eax ,%eax ,%ebx ,%eax ,%eax ,%eax ,%eax ,%eax # (J * 3) # ((j * 3) + k) # ((j * 3) + k) * 2 # ((j * 3) + k) * 4 # (((j * 3) + k) * 4) + L # ((((j * 3) + k) * 4) + L) * 2 movl %eax,%ebx movl Array(%ebx),%eax In questo esempio si è cercato di ridurre al massimo l'utilizzo di MUL e di utilizzare il registro %eax più ottimizzato e istruzioni accoppiabili che ne parlero più tardi. Altre Errori comuni Prendiamo in esame questo esempio a = a + b; normalmente un principiante codificherebbe questo con : movl a movl b addl %ebx , %eax , %ebx , %eax non va bene in quanto possiamo godere della proprietà commutativa : movl a addl b ,%eax ,%eax # Winner ! Dicevamo che il regitro eax è più ottimizzato degli altri notate questi esempi : add $2,al add $2,bl add $2,bx add $2,ax (gdb) disass _start Dump of assembler code for function _start: 0x0804810c <_start+0>: add $0x2,%al 0x0804810e <_start+2>: add $0x2,%bl 0x08048111 <_start+5>: add $0x2,%eax 0x08048114 <_start+8>: add $0x2,%ebx 0x08048117 <_start+11>: mov $0x1,%eax 0x0804811c <_start+16>: xor %ebx,%ebx 0x0804811e <_start+18>: int $0x80 End of assembler dump. (gdb) 2 3 3 3 Vincitore ! Come avete visto, add $0x2,%al è un byte più piccola della precedente ! Ora guardate questo disassemblaggio curioso : (gdb) disass _start Dump of assembler code for function _start: byte 0x0804810c 0x0804810e 0x08048110 0x08048112 0x08048116 0x08048117 0 1 2 3 4 5 6 7 8 9 10 11 12 13 <_start+0>: <_start+2>: <_start+4>: <_start+6>: <_start+10>: <_start+11>: inc add inc add inc add 0x0804811a <_start+14>: mov 0x0804811f <_start+19>: xor 0x08048121 <_start+21>: int End of assembler dump. (gdb) %al $0x1,%al %ax $0x1,%ax %eax $0x1,%eax lunghezza 2 2 2 4 1 3 Vincitore ! $0x1,%eax 1 %ebx,%ebx $0x80 In questo caso sembra che lavorando a 32 bit su un architettura a 32 bit sia meglio ottimizzato ! altro esempio : a=a-(b+c ); Questa espressione equivale ad : a = a – b – c ; movw a subw k subw c movw %ax ,%ax ,%ax ,%ax ,a E' preferibile un modo più efficiente per computare questa espressione : movw b addw c subw %ax , %ax , %ax ,a Risparmiando una istruzione risulta più compatta ed efficiente. ( proprietà commutativa ) altro esempio : if a = 0 then cmpl $0,%eax jz ... meglio test %eax,%eax jz ... # evitiamo i cicli per caricare il valore immediato # orl %eax,%eax scrive sul registro %eax # riducendo l'accoppiamento (vedi programmazione superscalare) Divide et impera ! Per moltiplicare o dividere, senza utilizzare mul o div un numero possiamo utilizare gli opratori scorrimento a destra e a sinistra, fate riferimento allo schema, sono molto veloci e compatti : moltiplicazione divisione SHLL $1,%EAX %EAX = %EAX * 2 SHLR $1,%EAX %EAX = %EAX / 2 SHLL $2,%EAX %EAX = %EAX * 4 SHLR $2,%EAX %EAX = %EAX / 4 SHLL $3,%EAX %EAX = %EAX * 8 SHLR $3,%EAX %EAX = %EAX / 8 SHLL $4,%EAX %EAX = %EAX * 16 SHLR $4,%EAX %EAX = %EAX / 16 SHLL $5,%EAX %EAX = %EAX * 32 SHLR $5,%EAX %EAX = %EAX / 32 SHLL $6,%EAX %EAX = %EAX * 64 SHLR $6,%EAX %EAX = %EAX / 64 SHLL $7,%EAX %EAX = %EAX * 128 SHLR $7,%EAX %EAX = %EAX / 128 SHLL $8,%EAX %EAX = %EAX * 256 SHLR $8,%EAX %EAX = %EAX / 256 ora possiamo anche moltiplicare per numeri differendi da quelli indicati : moltiplicare per 10 : movl shll movl shll addl $1 ,%eax %1 ,%eax %eax,%ebx %2 ,%eax %ebx,%eax # # # # moltiplica salva il risultato moltiplica aggiungi %eax * 2 %eax = 2 %eax * 4 %ebx %eax = 8 %eax = 10 %eax * 8 %eax = 8 %eax = 7 moltiplicare per 7 movl movl shll subl $1 ,%eax %eax,%ebx $3 ,%eax %ebx,%eax # moltiplica # Usare LEA per moltiplicare L'utilizzo di LEA (Load Effective Addres) con l'aiuto degli indici porta a volte a notevoli vantaggi per quanto riguardo la moltpiplicazione. Assumiamo che inizialmente %eax sia equivalente a 1 leal (%eax,%eax),%eax %eax = %eax + %eax risultato 2 SHLL $1,%EAX leal (%eax,%eax,2),%eax %eax = %eax + %eax*2 risultato 3 leal (%eax,%eax,4),%eax %eax = %eax + %eax*4 risultato 5 leal (%eax,%eax,8),%eax %eax = %eax + %eax*8 risultato 9 leal (,%eax),%eax %eax = %eax risultato 1 leal (,%eax,2),%eax %eax = %eax*2 risultato 2 SHLL $1,%EAX leal (,%eax,4),%eax %eax = %eax*4 risultato 4 SHLL $2,%EAX leal (,%eax,8),%eax %eax = %eax*8 risultato 8 SHLL $3,%EAX Per quanto riguarda la divisione sfortunatamente le cose non stanno così, se utilizzassimo gli shit e le sottrazioni andremo incontro a risultati sbagliati; Come dicevo in precedenza meglio moltiplicare che dividere quindi : 100 / 10 = 100 * 0.1. Programmazione Super Scalare e Pipeline Quello che distingue la famiglia Pentium dalle precedenti, sono alcune novità implementate nella sua architettura : 1) l'architettura super scalare ; 2) la previsione dei salti ; 3) L'ottimizzazione sull'esecuzione dei cicli. Il processore Pentium è costituito da due pipeline denominate U e V : – – U questa pipeline è in grado solo di eseguire istruzioni intere ; V è in grado di eseguire solo istruzioni semplici ; Tramite le due pipeline, quando possibile il pentium accoppia la loro esecuzione e quindili esegue contemporaneamente. Perciò superscalare significa quando un processore è dotato di due o più pipeline in parallelo. Le istruzioni semplici sono : MOV, ADD, SUB, CMP, AND, OR, INC, DEC, PUSH, POP, LEA, NOP, CALL, JMP, SHR, SHL, STC, CLC, XCHG, NOT, NEG. non sempre queste istruzioni possono essere accoppiate, vi sono alcune resitrzione da ricordare : – due istruzioni possono essere accoppiate se entrambe leggono lo stesso registro scrivono ; N.B. anche se scrivo su AH o AL comunque scrivo sul registro EAX. ma non lo – – – – – entrambe le istruzioni devono essere semplici ; gli scorrimenti e le rotazioni possono essere eseguiti solo nella pipe U ; le istruzioni ADC e SBB possono essere eseguite solo nella pipe U ; le istruzioni JMP e CALL possono essere eseguite solo nella pipe V ; nessuna delle due istruzioni può contenere uno scostamento o un operando immediato movl $2,(,%ebx,2) oppure movw $4,var ; – le istruzioni della pipe u devono occupare un solo byte, e sono accoppiate solo dalla seconda lettura (cache) ; quindi a causa di questa regola le istruzioni che possono essere accoppiate subito sono INC/DEC reg e PUSH/POP reg e NOP. Previsione dei salti Quando il pentium incontra un'istruzione JMP o CALL tenta attraverso la sua cache di prevedere la destinazione finale se questo funziona l'istruzione viene eseguita in un solo ciclo. Il pentium conserva in un buffer (BTB) il risultato dei primi 256 salti e tenta con questo di prevedere la destinazione. Nel pentium vi sono due code di PREFETCH , (32 byte) nella prima fase prima coda di prefetch se la verifica è corretta la seconda coda inizia già a leggere (i prossimi 32 byte) dalla destinazione. Se la previsione è errata le code verranno svuotate riattivando il prefetch. Quindi l'ottimizzazione migliore nel pentium per quanto riguarda i salti è : – – eseguire sempre gli stessi salti ; non eseguire mai i salti . Pipeline in virgola mobile Ogni istruzione deve essere letta dalla memoria (FECTH) per essere eseguita, questo processo è uno dei colli di bottiglia più importanti per quanto riguarda l'ottimizzazione, in quanto per quanto sia veloce la cpu deve aspettare per caricare i dati dalla memoria molto più lenta. La famiglia x86 si è sempre dotata di un piccolo buffer, coda di prefetch, dove leggeva le istruzioni successive dalla memoria con la tecnica FIFO, first in first out. All'introduzione del 486, venne introdotta una memoria interna detta CACHE . Questa memoria è una piccolo blocco di byte, a letura molto veloce che contiene una parte del programma di 32, 64 byte o dipendente dalla capacità della cache. Generalmente l'esecuzione del programma è sequenziale, quindi la cpu richiederà le istruzioni successise, queste essendo già nella cache velocizzerà di molto l'esecuzione. La cache utilzzo il blocco di memoria, più utilizzato quando questa è piena viene scartato il blocco di memoria meno utilizzato. Ci sono anche algoritmi per la gestione dei salti e la capacità come abbiamo visto di prevederli. Ora vediamo in dettaglio l'esecuizione di una singola istruzione anche semplice. movl (%ebx),%eax – – – – – – legge l'istruzione (fetch) ; decodifica ; calcola l'indirizzo effettivo ; legge dalla memoria ; salva i dati in %eax. calcolare l'indirizzo della nuova istruzione Non tutte le istruzioni eseguono questo procedimento, tuttavia è possibile ricondurre il funzionamente della macchina in 5 operazini fondamentali : – – – – – (R) Lettura (prefetch) ; (D) decodifica ; (I) generazione indirizzo ; (X) esecuzione ; (W) scrittura ; Immaginiamo queste 5 fasi come 5 strade, in cui scorrono le automobili. Ogni automobile equivale ad un'istruzione, ci sono alcune vetture più veloci che altre ed altre come tir più lente che trasportano più dati. Non proporiamente ma l'esecuzione delle istruzioni corre in parallelo su queste strada, quindi mentre in un autostrada transita una vettura, (istruzione) ed esegue al casello la prima fase Lettura, per poi portarsi alla fase della decodifica una seconda auto in coda può accedere alla prima fase. questo è lo schema di elaborazione delle istruzioni : cmpl $1 ,%eax je ... movl %1 ,%eax xorl %ebx,%ebx int $0x80 cicli Lettura Decodifica indirizzo esecuzione scrittura inizio 1 1 cmp $1,%eax 2 je ... cmp $1,%eax 3 movl %1,%eax je ... cmp $1,%eax 4 xorl %ebx,%ebx movl %1,%eax je ... cmp $1,%eax 5 int $0x80 xorl %ebx,%ebx movl %1,%eax je ... cmp $1,%eax xorl %ebx,%ebx movl %1 ,%eax je ... xorl %ebx,%ebx movl %1 ,%eax 6 7 fine 1 Questo è un diagramma perfetto, nel senso che se il programma potesse girare sempre in questo modo avremmo sicuramente la massima velocità, grazie alle pipeline; sfortunamtamente se in una corsia abbiamo un tir che rallenta il traffico, l'intera autostrada subirà un rallentamente anche se suddivisa in 5 corsie. Quando un'istruzione ritarda una fase di un ciclo si dice che la pipeline entra in stallo ! cicli Lettura Decodifica indirizzo esecuzione scrittura 1 movl var,%ebx 2 movl $1,%eax movl var,%ebx 3 movw $10,%cx movl $1,%eax movl var,%ebx 4 addl %cx,(%ebx) movw $10,%cx movl $1,%eax movl var,%ebx addl %cx,(%bx) movw $10,%cx movl $1,%eax movl var,%ebx fine 1 6 addl %cx,(%bx) movw $10,%cx movl $1,%eax stallo 3 7 addl %cx,(%bx) movw $10,%cx movw $10,%cx 5 movw $10,%cx Il ritardo delle istruzioni non solo avviene nella fase di esecuzione, ma può avvenire anche nella fase di generazione di un indirizzo, prendiamo come riferimento questo esempio : movl var , %ebx lea (%ebx,2),%ebx # carica la variabile nel registro # ne esegue la moltplicazione per 2 In questo caso se le istruzioni fossero eseguite contemporaneamente il risultato sarebbe senz'altro errto. Il pentium e 486 rilevano questa condizione e generano un Blocco AGI (Address Generation Interlock). Un blocco AGI viene generato quando : Come componente di un indirizzo viene utilizzato un registro e (%ebx) tale registro è la destinazione dell'istruzione che si trova al ciclo precedente (mow var,%ebx). Le pipeline lavorano insieme nel vero senso della parola, purtroppo se entra in stallo un anche l'altra subisce la stessa sorte : – – – quando un'istruzione in un pipe provoca uno stallo, entrambe entrano in stallo ; quando la pipe U durante la fase X entra in stallo, anche la V entra in stallo ; quanto la pipe V durante la fase X entra in stallo, la V riesce a completare x ; Ritardi Possiamo classificare i ritardi del pentium in 4 tipi di categorie, tra l'altro alcuni li ho già accennati. – – – – Conflitti nella memoria cache ; Blocchi AGI ; Ritardo per il byte di prefisso ; ritardi di sequenza. Il conflitto nella memoria cache accade quando due istruzioni accoppiate accedono allo stesso banco di memoria cache, perciò nella seconda istruzione viene instrodotto un ritardo di un ciclo. Per quanto riguarda i blocchi AGI ne ho parlato precedentemente. Il byte di prefisso è il byte che consente di superare i limiti del segmento, quindi richiede un ciclo in più : movw %cx,%dx # 1 ciclo movw %ax,%es:var # 1 + 1 cicli #------------3 Ricordate che un'istruzione con il byte di prefisso, tranne che per alcuni casi particolari, non è mai accoppiabile. In questo caso riordinando le istruzioni possiamo risparmiare un ciclo : movw %ax,%es:var # 1 + 1 cicli movw %cx,%dx # 0 ciclo #------------2 Il ritardo di sequenza è dovuto alla sequenza di esecuzione delle istruzioni. guardate attentamente questo codice : movl (%ebx), %eax # 1 ciclo addl $2 , $eax # 1 ciclo movl %eax ,(%ebx) # 1 ciclo movl (%edi), %eax # 0 ciclo addl $2 , $eax # 1 ciclo movl %eax ,(%edi) # 1 ciclo #----------5 cicli Riordinando la sequenza delle istruzioni senza cambiare il significato del codice possiamo ottenere un incremento di prestazioni ; movl movl addl addl movl movl (%ebx) (%edi) $2 $2 %eax %ecx ,%eax ,%ecx , $eax , $ecx ,(%ebx) ,(%edi) # 1 ciclo # 0 ciclo abbiamo aggiunto un registro $ecx # 1 ciclo # 0 ciclo # 1 ciclo # 0 ciclo #--------------3 cicli con accoppiamento ! Pipeline e virgola mobile Molte delle ottimizzazioni del Pentium sono state effettuate sui numeri in virgola mobile, molte istruzioni vengono eseguite all'incirca in 3 cicli, ma altre come la divisione o più complesse impiegano ancora molti cicli. Anche l'unità in virgola mobile può lavorare parallelamente con le istruzioni, e ancor più mentre vengono eseguite le istruzioni all'interno del coprocessore matematico, la cpu può continuare l'esecuzione del programmma : fldl real4 fsqrt fstl real4 addl $4,%edi subl $4,%ecx # 1 # 1 loopnz ... in questo modo diminuiamo i tempi di attesa, in quanto le istruzioni vengono eseguite parallelamente al calcolo della radice quadrata. fldl real4 fsqrt addl $4,%edi subl $4,%ecx fwait # *1 # 0 # 0 # *2 # *2 # attendi finchè fsqrt ha finito. fstl real4 loopnz ... Le istruzioni contrassegnate con *1 e *2 vengono eseguite contemporaneamente, in quanto l'unità in virgola mobile esegie fsqrt e la cpu esegue intanto addl e subl. CAPITOLO 16 Sfidare il Compilatore Sfidare il Compilatore E si ! tanto vale programmare in assembler se poi un compilatore ottimizza il codice meglio di noi, allora preferisco programmare in un linguaggio ad alto livello e ottimizzarlo con opportuni algoritmi, non è un impresa facile in quanto oggi i compilatori riescono a strizzare il codice ed operare accorgimenti molto validi, tuttavia adoperano degli schemi fissi per quanto riguarda la traduzione del codice in opcode, benchè molto efficienti è possibile con una buona conoscenza ed esperienza arrivare a battere il compilatore ! Tutti gli esempi verranno trattati con l'assembly inline, direttamente da gcc , per valutare le prestazioni mi avvalgo del timer software RDTSC valido, anche se esistono i software specializzati quali i profiler per evidenziare le caratteristiche del codice, per quello che è la velocità delle singole funzioni , i colli di bottiglia ed altro. Altri esempi utilizzo il semplice timer del pc confrontando il risultato in secondi. n.b. Una parte più approfondita la potrete trovare nel Vol .2 (ottimizzazioni x86-64), oltre a fornire ulteriori informazioni relative all'assembler, aggiungo alcuni parametri utilida utilizzare nel gcc e alcuni tool per il benchmarking nonchè il profiler. Iniziamo #include <stdio.h> unsigned long long ReadTimer ( void ) ; int main ( void ) { unsigned long long start=0 ; unsigned long long stop =0 ; start = ReadTimer ( ) ; printf ("\n timer start : %llu ",start ); __asm__ ( "nop\n\t" “nop\n\t" "nop\n\t" ); stop = ReadTimer ( ) ; printf ("\n timer start : %llu ",stop ); printf ("\n timer diff : %llu\n",stop-start ); return 0 ; } Commento : Come al solito vi presento prima il programma in questione, poi passo alle spiegazioni. Questo piccolo programma utilizza l'istruzione RDTSC e CPUID all'interno del compilatore GCC , assembly inline, per conteggiare i cicli che intercorrono tra una chiamata e l'atra della routine ReadTimer. Se compilate questo codice e lo mandate in esecuzione, vedrete differenti risultati ?! questo accade per via dei vari interrupt che vengono generati della macchina e che delegano la cpu a fare altre cose. Occorre testare il programma o la routine più volte e prendere il risultato minore, in questo caso c'è la certezza (più o meno) che non sono accorsi interrupt. L'altra soluzione più rudimentale ma efficace l'ho presa in prestito dal timer software della macchina calcolando direttamente i secondi che impiega per eseguire una routine. Essendo molto veloce la macchina, ho testato i vari esempi in loop ripetitivi di diversi milioni di cicli per volta dipende dalla subroutine da testare, ottenendo un valore fisso del tempo impiegato per ciascuna, potendo valutare così più correttamente la velocità del codice. unsigned long long : informa il compilatore di gestire un numero a 64 bit edx:eax La routine ReadTimer si compone di una struttura che contiene il valore alto e basso del numero a 64 bit ritornato dall'istruzione CPUID e per non dovere usare scorrimenti l'ho impostato in una union per avere direttamente il valore di ritorno. L'assembler inline è molto semplice, va preceduto dalla direttiva __asm__ ( ) ; ed all'interno degli apici vanno messe le istruzioni in assembly con alcuni accorgimenti che vi presenterò. /***************************************************/ // /***************************************************/ unsigned long long ReadTimer ( void ) { struct _time32 { unsigned int high ; unsigned int low ; }; RDTSC union _utime { struct _time32 int32 ; unsigned long long int64 ; } utime ; __asm__(“cpuid\n\t” “cpuid\n\t” "rdtsc\n\t" "movl %%eax,%0\n\t" "movl %%edx,%1\n\t" “cpuid\n\t” “cpuid\n\t” : "=m" (utime.int32.low), "=m" (utime.int32.high) : /* no input */ : "%eax" , "%edx" ); } return utime.int64 ; L'assembly inline tratta l'istruzione cpuid così come è scritta i caratteri speciali identificano la fine della riga '\n' '\t' obbligatori oppure ';' Per poter eseguire rdtsc è consigliato di ricorrere due volte all'istruzione cpuid e successivamente richiamare cpuid altre 2 volte dopo la sua esecuzione. Come potete vedere i registri %eax %edx sono indicati con doppi %% è una convenzione dell'assembler inline. per ultime non meno importante trovare una serie di due punti ':' indicano : 1) parametri in output ; 2) parametri in input ; 3) registri clobber ; Questi parametri sono importanti per poter comunicare con il compilatore C; per indicare di emettere una variabile in output da un registro occorre utilizzare “=m”: "=m" (utime.int32.low) indica di prendere la prima variabile %0 all'interno dell' assembler inline e di metterla nella variabile indicata "=m" (utime.int32.high) indicata la medesima cosa ma riferita alla seconda variabile %1 . In questa routine non ci sono parametri in input, ma vi è l'indicazione che i registri (clobber) %eax %edx sono esclusivi, cioè quando viene generata la routine contenente l'assembly inline questi non vanno utilizzati o modificati. Iniziamo con la prima sfida I test li ho fatti su un pentium 4 3.2 giga hertz con 1 G/B di ram a seconda del tempo di macchina avete modificate il numero di ripetizioni dei cicli. Probabilmente quando avrò finito il libro, inizieranno ad essere commerciati i computer quantistici! La prima sfida si tratta di inizializzare il più velocemente possibile un array di centomila elementi con il numero 0. Vediamo come gestisce il GCC con l'ottimizzazione -O3 e la nostra routine in assembly. /* inizializzazione di un array di 100.000 elementi /* eseguito per 200.000 volte */ */ #include <stdio.h> #include <stdlib.h> void RoutineC ( void ) ; void RoutineASM ( void ) ; int dim = 100000 ; int main ( void ) { const unsigned long CONT = 200000 ; // 200.000 unsigned long i=0; unsigned long time_start = 0 ; unsigned long time_stop = 0 ; time ( &time_start ) ; printf ("\n timer start : %ld ",time_start ); for (i=0;i<CONT;i++) { RoutineC() ; }; time ( &time_stop ) ; printf ("\n timer stop : %ld ",time_stop ); printf ("\n timer diff : %ld \n ",time_stop-time_start ); time ( &time_start ) ; printf ("\n timer start : %ld ",time_start ); for (i=0;i<CONT;i++) { RoutineASM() ; }; time ( &time_stop ) ; printf ("\n timer stop : %ld ",time_stop ); printf ("\n timer diff : %ld \n ",time_stop-time_start ); return 0 ; } void RoutineC ( void ) /* inizializzare un array di 100.000 elementi */ { register long i = 0 ; long array[dim] ; for (i=0;i<dim;i++) array[i]=0 ; } void RoutineASM ( void ) /* inizializzare un array di 100.000 elementi */ { long array[dim] ; } __asm__( " pxor %%mm0 , %%mm0\n\t" " pxor %%mm1 , %%mm1\n\t" " xorl %%eax , %%eax\n\t" " movl $25000, %%edi\n\t" "init: movq %%mm0 , (%%ebx,%%eax,8)\n\t" " movq %%mm1 ,8(%%ecx,%%eax,8)\n\t" " addl $2 , %%eax\n\t" " cmpl %%edi , %%eax\n\t" " jl init\n\t" : /* */ : "b" (&array[0]) ,"c" (&array[0]) : "%esi" , "%edi" ) ; debian:~/source# gcc -O3 s2.c -o s2 debian:~/source# ./s2 timer start : 1130273238 timer stop : 1130273259 timer diff : 21 timer start : 1130273259 timer stop : 1130273263 timer diff : 4 debian:~/source# Questi sono i risultati del programma direi 1 – 0 per noi ! la routine in assembly è molto semplice si avvale delle proprietà multimediali per scrivere in memoria 8 byte alla volta, quindi il conteggio del ciclo è notevolmente ridotto di un ¼. Anche utilizzando un solo registro non cambia. Ho cercato questa soluzione per evitare conflitti di banco e utilizzo di registri in scrittura. ma con scarsi risultati dala precedente ad un registro mm0. Comunque lo definisco un buon risultato. In questa routine non ci sono variabili di output, ma ne vediamo una in input :: "b" (&array[0]) ,"c" (&array[0]), identifica rispettivamente che in registri %ebx (b) e %ecx (c) verranno inizializzati con l'indirizzo del vettore. Nomeclatura dei registri : r Registri a %eax, %ax, %al b %ebx, %bx, %bl c %ecx, %cx, %cl d %edx, %dx, %dl S %esi, %si D %edi, %di "b" (&array[0]), sempre a riguarda a questa nomenclatura abbiamo u ulteriore sintassi "r" (x) in questo caso la r sta a significare un registro qualunque, quindi la nostra variabile verrà messa in un registro (%eax,%ebx,%ecx,%edx). Ora vediamo come è stata assemblata la nostra routine ASM : "&r" (x) “r” (y) in questo caso la r sta a significare un registro qualunque, quindi la nostra variabile verrà messa in un registro (%eax,%ebx,%ecx,%edx). Ora vediamo come è stata assemblata la nostra routine ASM, ma a differenza del precedente indica a gcc di non utilizzare lo stesso registro per le due variabili. .globl RoutineASM .type RoutineASM, @function RoutineASM: pushl %ebp movl %esp , %ebp subl $24 , %esp movl %ebx , -12(%ebp) movl %esp , %edx movl dim , %ebx movl %esi , -8(%ebp) movl %edi , -4(%ebp) decl %ebx leal 19(,%ebx,4), %ecx andl $-16 , %ecx subl %ecx , %esp movl %esp , %ebx movl %esp , %ecx #APP pxor %mm0 ,%mm0 pxor %mm1 ,%mm1 xorl %eax ,%eax movl $25000,%edi init: movq %mm0 , (%ebx,%eax,8) movq %mm1 ,8(%ecx,%eax,8) addl $2 , %eax cmpl %edi ,%eax jl init #NO_APP Come potete vedere, la nostra routine è all'interno del assembly del gcc indicato da due apici : #APP partenza #NOAPP stop. movl movl movl movl movl popl ret %edx -12(%ebp) -8(%ebp) -4(%ebp) %ebp, %ebp , %esp , %ebx , %esi , %edi %esp .size RoutineASM, .-RoutineASM .section .note.GNU-stack,"",@progbits .ident "GCC: (GNU) 3.3.5 (Debian 1:3.3.5-13)" Per completezza ho voluto mettere anche la parte terminale del codice. In neretto ci sono i clobber register ripristinati. Proviamo a sfidare ora strcpy Copiamo una stringa lunga 128 caratteri per 200 milioni di volte ! /* strcpy */ /* copia 128 caratteri per 200 milioni di volte */ #include <stdio.h> #include <stdlib.h> void RoutineC ( void ) ; void RoutineASM ( void ) ; int main ( void ) { const unsigned long CONT = 200000000 ; // 200.000.000 cento milioni di volte unsigned long i=0; unsigned long time_start = 0 ; unsigned long time_stop = 0 ; time ( &time_start ) ; printf ("\n timer start : %ld ",time_start ); for (i=0;i<CONT;i++) { RoutineC() ; }; time ( &time_stop ) ; printf ("\n timer stop : %ld ",time_stop ); printf ("\n timer diff : %ld \n ",time_stop-time_start ); time ( &time_start ) ; printf ("\n timer start : %ld ",time_start ); for (i=0;i<CONT;i++) { RoutineASM() ; }; time ( &time_stop ) ; printf ("\n timer stop : %ld ",time_stop ); printf ("\n timer diff : %ld \n ",time_stop-time_start ); return 0 ; } void RoutineC ( void ) /* inizializzare un array di 100.000 elementi */ { char *source = "0123456789ABCDEF0123456789ABCDEF" \ "0123456789ABCDEF0123456789ABCDEF" \ "0123456789ABCDEF0123456789ABCDEF" \ "0123456789ABCDEF0123456789ABCDE\0" ; // 128 caratteri char dest[128] ; strcpy ( &dest[0] , &source[0] ); } void RoutineASM ( void ) /* inizializzare un array di 100.000 elementi */ { char *source = "0123456789ABCDEF0123456789ABCDEF" \ "0123456789ABCDEF0123456789ABCDEF" \ "0123456789ABCDEF0123456789ABCDEF" \ "0123456789ABCDEF0123456789ABCDE\0" ; // 128 caratteri char dest[128] ; __asm__ ( " " " movl %%edi,%%ecx\n\t" shr $1,%%ecx\n\t" jnc cont2\n\t" "cont1: " " " " decl movb incl movb incl %%edi\n\t" (%%esi) ,%%al\n\t" %%edi\n\t" %%al ,(%%edi)\n\t" %%esi\n\r" "cont2: " " " movw addl movw addl (%%esi) ,%%ax\n\t" $2 ,%%esi\n\t" %%ax ,(%%edi)\n\t" $2 ,%%edi\n\r" " cmpb $0 ,%%al\n\t" " je exit\n\t" " cmpb $0 ,%%ah\n\t" " jnz cont2\n\t" "exit:\n\t" : /* no output */ : "D" (&dest[0]) , "S" (&source[0]) ) ; } commento : Ho preferito non utilizzare le comuni operazioni sulle stringhe LODSB STOSB in quanto queste pur essendo più lunghe sono più efficienti, c'è da notare che poi le operazioni sulle stringhe utilizzano il registro di segmento ES con un bytecode in più, quindi un ritardo. Algoritmo : Dato che non so a priori quanto la stringa è lunga e dato che per far prima mi conviene copiare WORD anzichè byte determino con la divisione il numero di byte iniziale della stringa ottenendo il resto se questo è 1 (carry significa che è dispari e copierò un byte e poi continuerò al ciclo principale viceversa partirà direttamente la copia con le word. In questo caso non ci sono clobber register ma solo due registri di input la sorgente (%esi) e la destinazione (%edi). Questo è il risultato : debian:~/source# gcc -O3 s4.c -o s4 debian:~/source# ./s4 timer start : 1130274695 timer stop : 1130274725 timer diff : 30 timer start : 1130274725 timer stop : 1130274745 timer diff : 20 debian:~/source# Possiamo definirci sul 2 – 0 in nostro favore ! Lascio a voi verificare la fattibilità delle operazione : – – orb %al,%al ; test %al,%al ; se c'è qualche guadagno in velocità Ora vi presento una versione avanzata della stessa routine, ottimizzata al fine di meglio gestire l'uso dei registri e di catturare 4 byte alla volta, la logica di funzionamento iniziale è quasi identica viene determinato il resto per vedere quante volte è divisibile la stringa per 4, quindi vengono copiati inizialmente i byte da 1 a 3, lasciando poi lavorare la routine vera e propria con i trasferimenti di 4 byte in 4 : void RoutineASM ( void ) { char *source = "01234567890ABCDEF0123456789ABCDEF" \ "01234567890ABCDEF0123456789ABCDEF" \ "01234567890ABCDEF0123456789ABCDEF" \ "01234567890ABCDEF0123456789ABCDE\0" ; // 128 caratteri char dest[128] ; __asm__ ( " .p2align 4,,15\n\t" " movl %%edi ,%%eax\n\t" " movl %%eax ,%%esi\n\t" " movl %%edi ,%%ecx\n\t" " and $3 ,%%ecx\n\t" " rep movsb\n\t" "cont1: sub $4 ,%%edi\n\t" " movl (%%esi) ,%%eax\n\t" " addl $4 ,%%edi\n\t" " movl %%eax ,(%%edi)\n\t" " addl $4 ,%%esi\n\r" " cmpb $0 ,%%al\n\t" " jne cont1\n\t" } : : ) ; /* no output */ "D" (&dest[0]) , "S" (&source[0]) Ecco i risultati : debian:~/source# ./s6 timer start : 1130275435 timer stop : 1130275467 timer diff : 31 timer start : 1130275467 timer stop : 1130275470 timer diff : 3 Questa è decisamente più performante, tuttavia rimaniamo sul punteggio di 2 – 0 con una buona azione gol ! Ora sfidiamo la strncpy Ho voluto fare questo esempio, in quanto se noi a priori possiamo determinare la lunghezza dei nostri dati riusciamo a gestire algoritmi più efficienti, come in questo caso : /* strncpy */ #include <stdio.h> #include <stdlib.h> void RoutineC ( void ) ; void RoutineASM ( void ) ; int dim = 100000 ; int main ( void ) { const unsigned long CONT = 200000000 ; // 400.000.000 cento milioni di volte unsigned long i=0; unsigned long time_start = 0 ; unsigned long time_stop = 0 ; time ( &time_start ) ; printf ("\n timer start : %ld ",time_start ); for (i=0;i<CONT;i++) { RoutineC() ; }; time ( &time_stop ) ; printf ("\n timer stop : %ld ",time_stop ); printf ("\n timer diff : %ld \n ",time_stop-time_start ); /*******/ time ( &time_start ) ; printf ("\n timer start : %ld ",time_start ); for (i=0;i<CONT;i++) { RoutineASM() ; }; time ( &time_stop ) ; printf ("\n timer stop : %ld ",time_stop ); printf ("\n timer diff : %ld \n ",time_stop-time_start ); return 0 ; } void RoutineC ( void ) { char *source = "01234567890ABCDEF0123456789ABCDEF" \ "01234567890ABCDEF0123456789ABCDEF" \ "01234567890ABCDEF0123456789ABCDEF" \ "01234567890ABCDEF0123456789ABCDE\0" ; // 128 caratteri char dest[128] ; strncpy ( &dest[0] , &source[0] , 128 ); } void RoutineASM ( void ) /* inizializzare un array di 100.000 elementi */ { char *source = "01234567890ABCDEF0123456789ABCDEF" \ "01234567890ABCDEF0123456789ABCDEF" \ "01234567890ABCDEF0123456789ABCDEF" \ "01234567890ABCDEF0123456789ABCDE\0" ; // 128 caratteri char dest[128] ; __asm__ ( " " " " " " " : : ) ; .p2align 4,,15\n\t" movdqu (%%eax) , %%xmm0\n\t" addl $8 , %%eax\n\t" movdqu %%xmm0 , (%%ebx)\n\t" movdqu (%%eax) , %%xmm1\n\t" addl $8 , %%ebx\n\t" movdqu %%xmm1 , (%%ebx)\n\t" /* no output */ "a" (&source[0]) , "b" (&dest[0]) } In questo caso ho fatto uso dei registri xmm0 per trasferirli più dati contemporaneamente, questo è il risultato : debian:~/source# gcc -O3 s7.c -o s7 debian:~/source# ./s7 timer start : 1130275894 timer stop : 1130275932 timer diff : 38 timer start : 1130275932 timer stop : 1130275935 timer diff : 3 debian:~/source# Direi ottimo! 3 – 0 senza discussioni ! Sfidiamo la STRLEN ! E qui si tratta di un osso duro, in quanto per diversi tentavi che ho fatto 3 ho sempre ottenuto un pareggio ! #include <stdio.h> #include <stdlib.h> /* strlen */ void RoutineC ( void ) ; void RoutineASM ( void ) ; int dim = 100000 ; // lunghezza array o stringa int main ( void ) { const unsigned long CONT = 4000000000 ; //1.000.000.000 unsigned long i=0; unsigned long time_start = 0 ; unsigned long time_stop = 0 ; time ( &time_start ) ; printf ("\n timer start : %ld ",time_start ); for (i=0;i<CONT;i++) { RoutineC() ; }; time ( &time_stop ) ; printf ("\n timer stop : %ld ",time_stop ); printf ("\n timer diff : %ld \n ",time_stop-time_start ); time ( &time_start ) ; printf ("\n timer start : %ld ",time_start ); for (i=0;i<CONT;i++) { RoutineASM() ; }; time ( &time_stop ) ; printf ("\n timer stop : %ld ",time_stop ); printf ("\n timer diff : %ld \n ",time_stop-time_start ); return 0 ; } void RoutineC ( void ) { char *source = "01234567890ABCDEF0123456789ABCDEF" \ "01234567890ABCDEF0123456789ABCDEF" \ "01234567890ABCDEF0123456789ABCDEF" \ "01234567890ABCDEF0123456789ABCDE\0" ; // 128 caratteri int l ; l = strlen ( &source[0] ) ; } void RoutineASM ( void ) { char *source = "01234567890ABCDEF0123456789ABCDEF" \ "01234567890ABCDEF0123456789ABCDEF" \ "01234567890ABCDEF0123456789ABCDEF" \ "01234567890ABCDEF0123456789ABCDE\0" ; // 128 caratteri unsigned int len = 0; __asm__ ( " .p2align 4,,15\n\t" " movl %%ebx ,%%edx\n\t" "l1: movb (%%ebx) ,%%ah\n\t" " incl %%ebx\n\t" " test %%ah ,%%ah\n\t" " jne l1\n\t" " subl %%ebx ,%%edx\n\t" : "=b" (len) : "d" (&source[0]) : "ebx", "edx" ) ; len = len ; } debian:~/source# gcc -O3 s8.c -o s8 s8.c: In function `main': s8.c:13: warning: this decimal constant is unsigned only in ISO C90 debian:~/source# ./s8 timer start : 1130276375 timer stop : 1130276393 timer diff : 18 timer start : 1130276393 timer stop : 1130276411 timer diff : 18 debian:~/source# Come potete vedere c'è un avvertimento in fase di compilazione è per i numeri troppo grandi non gestiti, li gestisce solo ISO C90, potrebbe non essere standard. Comunque il risultato è di parità. Nelle tre direttive filane gli ho detto che : – – – la variabile len deve contenere il valore del registro %EBX ; il registro %edx deve contenere l'indirizzo di partenza della stringa ; %ebx %edx clobber register. Non è servito nemmeno quel tipo di allineamento dei dati. Strlen atto 2 void RoutineASM ( void ) { char *source = "01234567890ABCDEF0123456789ABCDEF" \ "01234567890ABCDEF0123456789ABCDEF" \ "01234567890ABCDEF0123456789ABCDEF" \ "01234567890ABCDEF0123456789ABCDE\0" ; // 128 caratteri unsigned int len = 0; __asm__ (“ decl %%ecx\n\t" "l1: incl %%ecx\n\t" " test $3 , %%ecx\n\t" " jz l2\n\t" " cmpb $0 , %%ecx\n\t" " jne l1\n\t" " jmp l6\n\t" "l2: movl (%%ecx) , %%eax\n\t" " addl $4 , %%ecx\n\t" " test %%al ,%%al\n\t" " jz l5\n\t" " test %%ah ,%%ah\n\t" " jz l4\n\t" " test $0x00ff0000,%%eax\n\t" " jz l3\n\t" " test $0xff000000,%%eax\n\t" " jnz l2\n\t" " incl %%ecx\n\t" "l3: incl %%ecx\n\t" "l4: incl %%ecx\n\t" "l5: subl $4 ,%%ecx\n\t" "l6: subl %%ecx ,%%ebx\n\t" : "=b" (len) : "c" (&source[0]), "b" (&source[0]) ) ; len = len ; } Ho provato anche questa ulteriore soluzione, ma il tempo è rimasto invariato; Questa soluzione è molto valida e la consiglio vivamente. strlen atto 3 Una soluzione migliore è rappresentata dalla routine seguente void RoutineASM ( void ) { char *source = "01234567890ABCDEF0123456789ABCDEF" \ "01234567890ABCDEF0123456789ABCDEF" \ "01234567890ABCDEF0123456789ABCDEF" \ "01234567890ABCDEF0123456789ABCDE\0" ; // 128 caratteri unsigned int len = 0; __asm__ ( " pushl %%edi\n\t" " " " " " " addl cmpb jz subl jns jmp $1,%%eax\n\t" $0,(%%eax)\n\t" lbl1\n\t" $1,%%ecx\n\t" lbl0\n\t" lbl2\n\t" " lbl1:\n\t" " " " " " " " " " " " subl %%ebx,%%eax\n\t" jmp lbl5\n\t" pushl %%esi\n\t" movl %%ebx,%%eax\n\t" movl %%eax ,%%ecx\n\t" addl $3,%%ecx\n\t" andl $0xFFFFFFFC,%%ecx\n\t" subl %%eax,%%ecx\n\t" movl %%ecx,%%esi\n\t" jz lbl2\n\t" subl $1,%%eax\n\t" " lbl0:\n\t" " lbl2:\n\t" " leal 3(%%eax),%%edx\n\t" " nop\n\t" " lbl3:\n\t" "movl (%%eax),%%edi\n\t" "addl $4,%%eax\n\t" "leal -0x1010101(%%edi),%%ecx\n\t" "notl %%edi\n\t" "andl %%edi,%%ecx\n\t" "andl $0x80808080,%%ecx\n\t" "jz lbl3\n\t" "test $0x8080,%%ecx\n\t" "jnz lbl4\n\t" "shrl $10,%%ecx\n\t" "addl $2 ,%%eax\n\t" "lbl4:\n\t" "shlb $1,%%cl\n\t" "sbbl %%edx,%%eax\n\t" "addl %%esi,%%eax\n\t" "lbl5:\n\t" "popl %%esi\n\t" "popl %%edi\n\t" : "=a" (len) : "b" (&source[0]) ) ; len = len ; } Tuttavia la situazione rimane immutata sempre di parità ! (questa soluzione è stata postata su (comp.lang.asm.x86) Quasi rassegnato ho provato, la gestione istruzioni stringa da parte del compilatore, portando sempre ad uno stesso tempo, a questo punto il consiglio mio è di utilizzare quest' ultime a pari velocità c'è più compattezza di codice ! void RoutineASM ( void ) { char *source = "01234567890ABCDEF0123456789ABCDEF" \ "01234567890ABCDEF0123456789ABCDEF" \ "01234567890ABCDEF0123456789ABCDEF" \ "01234567890ABCDEF0123456789ABCDE\0" ; // 128 caratteri unsigned int len = 0; __asm__ ( ) ; len = len ; } xorl %%eax,%%eax\n\t" "c1: lodsb\n\t" " cmpb $0,%%al\n\t" " jnz c1\n\t" : "=a" (len) : "S" (&source[0]) Ottimizzare calcoli matrici In questo esempio vedremo come è possibile ottimizzare il calcolo delle matrici i risultati sono buoni. void RoutineC ( void ) /* inizializzare un array di 100.000 elementi */ { const int d = 100 ; int i,j,k ; int a[d][d] , b[d][d] , c[d][d] ; for (i=0;i<d;i++) { for (j=0;j<d;j++) { a[i][j] = b[i][j] * c[i][j] ; } } } void RoutineASM ( void ) { const int d = 100 ; int i,j,k ; int a[d][d] , b[d][d] , c[d][d] ; __asm__ ( ) ; " xorl %%ebx , %%ebx ; " " movl $100 , %%eax ; " "c0: pushl %%eax ; " "c1: movl (%%ecx,%%ebx,4) , %%eax ; " " imull (%%edi,%%ebx,4) ; " " movl %%eax , (%%esi,%%ebx,4) ; " " addl $1 , %%ebx ; " " cmpl $100 , %%ebx ; " " jle c1\n\t" " addl $400,%%ecx ;" " addl $400,%%edi ;" " addl $400,%%esi ;" " popl %%eax ;" " decl %%eax ;" " cmpl $0 , %%eax;" " jnz c0 ;" : /* output */ : "c" (&a[0][0]), "S" (&b[0][0]), "D" (&c[0][0]) : "ebx" } Di particolare da evidenziare c'è solamente la somma del valore 400 che equivale a 100 elementi di un array long (100*4) evitando cosi altri controlli dei cicli. Il registri %ebx funziona come secondo indice e dato che non avevo a disposizione più registri, in quanto edx viene modificato nella moltiplicazione ho utilizzato un push e pop. CAPITOLO 17 Grafica che passione ! Grafica che passione ! In questo capitolo inizieremo lo studio della grafica su linux. Esistono moltissime librerie a cui riferirsi già compilate che permettono tutte attraverso codice sorgente di accedere agli elementi base della grafica, io ho scelto le SDL (Simple Direct Media Layer) un ottimo tool graficico di largo utilizzo. In debian è incluso come pacchetto e non dovete far altro che installare le librerie di sviluppo : – – – – – libsdl1.2debian libsdl1.2debian-all libsdl1.2-dev libsdl1-console libsdl1-console-dev E altre librerie al momento del loro utilizzo. Per quanto riguarda gli header file li potete trovare in : – usr/include/SDL/SDL.h Da qui vengono richiamati altri file di inclusione, potete trovare tutti i valori delle costanti che vengono passate come parametro alle varie subroutine.Esistono molti esempi a riguardo di queste librerie, tutti scritti in 'C', ma non è difficile tradurli in assembler. SDL e' una libreria multimediale cross-platform utilizzata per esempio anche applicativi commerciali, per portare i giochi da windows a linux. SDL supporta la maggior parte dei sistemi operativ e non solo al suo interno contiene librerie per la grafica, ma si spazia al suono, al multitreading al joystic alla rete e cosi via, è una piattaforma ideata e creata per gestire i giochi. Oggigiorno i giochi sono sempre più esosi di memoria e avidi di velocità, non mi dilungo quindi sulle peculiarità senza dubbio ottime di questa librearia. http://www.libsdl.org per ulteriori informazioni e tools che vanno a completare la libreria. Iniziamo col primo listato. #include <SDL/SDL.h> #include <stdio.h> #include <stdlib.h> int main ( void ) { SDL_Surface *screen ; // iniziallizza il sistema video di SDL e controlla eventuali errori if (SDL_Init( SDL_INIT_VIDEO) != 0) { printf ("\nUnable to initialize SDL: %s\n",SDL_GetError() ) ; return 1 ; } // assicurati che SDL_Quit venga chiamato quanto il programma termina atexit ( SDL_Quit ) ; screen = SDL_SetVideoMode( 640,480,16,SDL_FULLSCREEN ) ; if ( screen == NULL ) { printf ("Unable to set video mode: %s\n",SDL_GetError()); return 1 ; } printf ("\nSuccess!"); return 0 ; } Questo è un tipico programma scritto in C, questo programma non fa nient'altro che modificare le dimensioni video a 640,480 con 16 colori e controlla le avvenute operazioni od esce con un messaggio di errore. Compilazione : prog.c gcc -I/usr/include/SDL -D_REENTRANT -L/usr/lib -lSDL -lpthread prog.c -o prog Il programma include l'header SDL.h, l'header principale dove include altri sotto header. Definisce un puntatore alla struttura SDL_Surface, che è poi l'area in cui andremo a gestire e la salva in un puntatore. Segue l'inizializzazione della libreria e quindi controlla che viene supportata la modalità grafica. Quindi predispone lo schermo per la modalità prescelta. Ttraduzione in Assembler .equ .equ .equ SDL_INIT_VIDEO, 0x00000020 SDL_FULLSCREEN, 0x80000000 SDL_Quit , 0x080484c4 .section .data # SDL_Surface *screen ; screen: .long 0 ERR_InitVideo: .asciz "\nUnable to initialize SDL: %s\n" ERR_SetVideoMode: .asciz "Unable to set video mode: %s\n" MSG_Success: .asciz "\nSuccess!\n" .section .text main: .globl main # iniziallizza il sistema video di SDL # e controlla eventuali errori pushl $SDL_INIT_VIDEO call SDL_Init testl %eax,%eax jz InitVideo_OK call SDL_GetError pushl %eax pushl $ERR_InitVideo call printf addl $8,%esp InitVideo_OK: # assicurati che SDL_Quit # venga chiamato quanto il # programma termina pushl $SDL_Quit call atexit pushl $SDL_FULLSCREEN pushl $16 pushl $480 pushl $640 call SDL_SetVideoMode addl $32,%esp movl %eax,(screen) testl %eax,%eax jnz SetVideoMode_OK call SDL_GetError pushl %eax pushl $ERR_InitVideo call printf addl $8,%esp movl $1,%eax movl $1,%ebx int $0x80 SetVideoMode_OK: pushl $MSG_Success call printf addl $4,%esp movl $1,%eax xorl %ebx,%ebx int $0x80 movl $1,%eax movl $1,%ebx int $0x80 as --gstabs prog.s -o prog.o gcc -I/usr/include/SDL -D_REENTRANT -L/usr/lib -lSDL -lpthread -lc prog.o -o prog.bin La traduzione in assembler non è poi tanto più complessa , possiamo creare da soli un file di inclusione con tutte le variabili che ci servono, io li creo man mano che utilizzo le routine. .equ .equ .equ SDL_INIT_VIDEO, 0x00000020 SDL_FULLSCREEN, 0x80000000 SDL_Quit , 0x080484c4 Questa è la parte in assembler in cui viene dichiarato il puntatore per contenere la struttura surface, in assembler abbiamo bisogno solo di creare un indirizzo sarà poi la funzione chiamante che ci ritorna la posizione della struttura. .section .data # SDL_Surface *screen ; screen: .long 0 Seguono le stringhe di errore e eventuali messaggi informativi. main: .globl main Questo è importante in quanto la gestione del link l'affidiamo al gcc quindi meglio optare per un punto di ingresso a lui confacente appunto main. pushl $SDL_INIT_VIDEO call SDL_Init testl %eax,%eax jz InitVideo_OK call SDL_GetError pushl %eax In questa parte di programma viene chiamata la funzione di inizializzazione del video, se questa ritorna un valore negativo qualcosa è andato a male, allora viene chiamata la routine GetError, che ritorna il messaggio di errore in %eax. # screen = SDL_SetVideoMode( 640,480,16,SDL_FULLSCREEN ) ; pushl $SDL_FULLSCREEN pushl $16 pushl $480 pushl $640 call SDL_SetVideoMode addl $32,%esp movl %eax,(screen) Questa è la traduzione della stringa in 'C' soprastante, ricordate di mettere i parametri in ordine inverso, una prova per curiosita fatelo e vedete che messaggio di errore vi riporta la funzione GetError. La restante parte del programma non ha particolari aspetti rilevanti. Vi fornisco la stringa di compilazione del programma. as --gstabs prog.s -o prog.o gcc -I/usr/include/SDL -D_REENTRANT -L/usr/lib -lSDL -lpthread -lc prog.o -o prog.bin Secondo listato Questo programma è un poco più complesso del primo in quanto prevede la gestione delle strutture, in assembler non è sempre facile riferirsi ai singoli elementi di una stuttura, soprattutto se queste incorporano altre strutture; io utilizzo un metodo personale passo passo, per la costruzione del codice assembler, al fine di evitare errori, di sequito riporto il secondo listato : #include <SDL/SDL.h> #include <stdio.h> #include <stdlib.h> Uint16 CreateHicolorPixel blue) { Uint16 value ; (SDL_PixelFormat *fmt,Uint8 red, Uint8 value = ((red >> fmt->Rloss) << fmt->Rshift) \ + ((green>> fmt->Gloss) << fmt->Gshift) \ + ((blue >> fmt->Bloss) << fmt->Bshift) ; return value; } ; int main ( void ) { SDL_Surface *screen ; Uint16 *raw_pixels ; int x,y ; Uint16 pixel_color ; int offset ; SDL_Init(SDL_INIT_VIDEO) ; atexit(SDL_Quit) ; screen = SDL_SetVideoMode( 640,480,8,0) ; SDL_LockSurface(screen) ; raw_pixels = (Uint16*) screen->pixels ; for (x=0;x<256;x++) { for (y=0;y<256;y++) { pixel_color = CreateHicolorPixel( screen->format,x,0,y) ; offset = (screen->pitch/2 * y + x) ; raw_pixels[offset] = pixel_color ; } } SDL_UnlockSurface(screen) ; SDL_UpdateRect(screen,0,0,0,0) ; SDL_Delay(3000); return 0 ; } green,Uint8 Riga di comando per compilare il listato : gcc -I/usr/include/SDL -D_REENTRANT -L/usr/lib -lSDL -lpthread prog.c -o prog Questo piccolo programma, non riveste particolare difficoltà nel tradurlo in assembler, c'è solo da prestare molta attenzione al corretto indirizzamento delle strutture e del relativo significato dei cast, vedi strutture sotto : (Uint16*) screen->pixels ; ( screen->format,x,0,y) ; (screen->pitch/2 * y + x) ; Un metodo molto comodo che utilizzo è proprio partire dal srogente 'C', quando si avrà preso più dimestichezza con la libreria, si potrà partire direttamente dallo scheletro di un sorgente in assembler. passo 1 : #1 #2 #include <SDL/SDL.h> #include <stdio.h> #include <stdlib.h> __asm__ ( "\n " : "=a" (raw_pixels) : "a" (screen->pixels) ); Uint16 CreateHicolorPixel (SDL_PixelFormat *fmt, Uint8 red, Uint8 green, Uint8 blue) { Uint16 value ; value = ((red >> fmt->Rloss) << fmt->Rshift) + ((green>> fmt->Gloss) << fmt->Gshift) + ((blue >> fmt->Bloss) << fmt->Bshift) ; return value; }; int main ( void { SDL_Surface Uint16 int Uint16 int ) *screen ; *raw_pixels ; x,y ; pixel_color ; offset ; // 4 // 60 byte // 2 // 4,4 // 2 __asm__ ( "\n\t pushl $32 " "\n\t call SDL_Init " "\n\t popl %%eax " : ); __asm__ ( "\n\t pushl %%eax " "\n\t call atexit " "\n\t popl %%eax " : : "a" (SDL_Quit) ); __asm__ ( "\n\t pushl $0 " "\n\t pushl $16 " "\n\t pushl $256 " "\n\t pushl $256 " "\n\t call SDL_SetVideoMode " "\n\t addl $16,%%esp " : "=a" (screen) : "a" (SDL_Quit) ); __asm__ ( "\n\t pushl %%eax " "\n\t call SDL_LockSurface " "\n\t popl %%eax " : : "a" (screen) ); __asm__ ( "\n\t\t movl $0 ,%%eax" :"=a" (x) :"a" (x) ); __asm__ ( "\n\t\t forx:" "\n\t\t cmpl $255,%%eax" " \n\t\t jle bodyx" "\n\t\t jmp nextx" "\n\t\t bodyx:" :"=a" (x) :"a" (x) ); __asm__ ( "\n\t\t movl $0 ,%%eax" :"=a" (y) :"a" (y) ); __asm__ ( "\n\t\t fory:" "\n\t\t cmpl $255,%%eax" "\n\t\t jle bodyy" "\n\t\t jmp nexty" "\n\t\t bodyy:" :"=a" (y) :"a" (y) ); __asm__ ( "\n\t\t\t pushl %%eax " "\n\t\t\t pushl $0 " "\n\t\t\t pushl %%ecx " "\n\t\t\t pushl %%edx " "\n\t\t\t call CreateHicolorPixel " "\n\t\t\t addl $16,%%esp " : "=a" (pixel_color) : "a" (y), "c" (x), "d" (screen->format) ); __asm__ ( "\n\t\t\t shrl %%eax " "\n\t\t\t movzwl %%ax ,%%eax " "\n\t\t\t imull %%ecx,%%eax " "\n\t\t\t addl %%edx,%%eax " : "=a" (offset) : "a" (screen->pitch) , "c" (y), "d" (x) ); #3 __asm__ ( "\n " : "=a" (raw_pixels[offset]) : "a" (pixel_color) ); __asm__ ( "\n\t\t incl %%eax" "\n\t\t jmp fory" "\n\t\t nexty:" :"=a" (y) :"a" (y) ); __asm__ ( "\n\t\t incl %%eax" "\n\t\t jmp forx" "\n\t\t nextx:" :"=a" (x) :"a" (x) ); __asm__ ("\n\t pushl %%eax " "\n\t call SDL_UnlockSurface " "\n\t popl %%eax " : : "a" (screen) ); __asm__ ( "\n\t pushl $0 " "\n\t pushl $0 " "\n\t pushl $0 " "\n\t pushl $0 " "\n\t pushl %%eax " "\n\t call SDL_UpdateRect " "\n\t addl $20,%%esp " : : "a" (screen) ); __asm__ ( "\n\t pushl $3000 " "\n\t call SDL_Delay " "\n\t popl %%eax " : ); return 0 ; } Questa è una prima traduzione del programma in C, in assembler passo 1, non si tratta di barare, piuttosto si utilizzano tutti i tools disponibili per arrivare al proprio scopo, certo l'opzione -S del compilatore ci aiutava moltissimo, ma ci toglieva il gusto della programmazione. A questo punto come secondo passo utilizzo l'opzione -S che dal mio pseudo sorgente in asminline genera un codice in assembly completo (ho tralasciato la traduzione della prima routine). Il seguente listato dopo il primo passo mostra l'effettiva traduzione in assembler, ancora è possibile migliorarlo con le opportune modifiche desiderate, oppure linkarlo con LD, in questo caso occorre sostituire $SDL_QUIT e le altri costanti incluse il corretto valore, fate riferimento al file di inclusioen che appare nella riga di compando di gcc per compilare il listato con le sdl. Esistono diverse liberie specifiche per ogni scopo, sta poi a voi scegliere la più adatta ai vostri scopi, SDL_Init desc : inizializza SDL system SDL_SetVideoMode desc : Crea una finestra o inizializza l'adattatore video per SDL. input : width : larghezza finestra height : altezza finestra bpp : risoluzione 8 15 16 24 32 flags : – SDL_FULLSCREEN; – SDL_DOUBLEBUF; – SDL_HWSURFACE; – SDL_OPENGL return : un puntatore a una struttura valida SDL – – – – SDL_Quit desc : termina le strutture di surface N.B. Non descriverò tutte le funzioni della libreria, ad alcune anche se le trovate nel listato non ne farò accenno, mi interessa solo interagire con le librerie tramite l'assembly e non scrivere un corso di programmazione con le SDL. In effetti sulla rete ci sono molte monografie scritte molto bene su come utilizzare le SDL. #include <SDL/SDL.h> #include <stdio.h> #include <stdlib.h> Uint16 CreateHicolorPixel (SDL_PixelFormat *fmt,Uint8 green,Uint8 blue) { Uint16 value ; red, Uint8 value = ((red >> fmt->Rloss) << fmt->Rshift) \ + ((green>> fmt->Gloss) << fmt->Gshift) \ + ((blue >> fmt->Bloss) << fmt->Bshift) ; return value; }; int main ( void ) { SDL_Surface *screen ; // 60 byte Uint16 *raw_pixels ; // 2 int x,y ; // 4,4 Uint16 pixel_color ; // 2 int offset ; // 4 __asm__ ( "\n\t pushl $32 " "\n\t call SDL_Init " "\n\t popl %%eax " : ); __asm__ ( "\n\t pushl %%eax "\n\t call atexit " "\n\t popl %%eax " : : "a" (SDL_Quit) ); " __asm__ ( "\n\t pushl $0 " "\n\t pushl $16 " "\n\t pushl $256 " "\n\t pushl $256 " "\n\t call SDL_SetVideoMode " "\n\t addl $16,%%esp " : "=a" (screen) : "a" (SDL_Quit) ); __asm__ ( "\n\t pushl %%eax " "\n\t call SDL_LockSurface " "\n\t popl %%eax " : : "a" (screen) ); __asm__ ( "\n " : "=a" (raw_pixels) : "a" (screen->pixels) ); __asm__ ( "\n\t\t movl $0 ,%%eax" :"=a" (x) :"a" (x) ); __asm__ ( "\n\t\t forx:" "\n\t\t cmpl $255,%%eax" "\n\t\t jle bodyx" "\n\t\t jmp nextx" "\n\t\t bodyx:" :"=a" (x) :"a" (x) ); __asm__ ( "\n\t\t movl $0 ,%%eax" :"=a" (y) :"a" (y) ); __asm__ ( "\n " : "=a" (raw_pixels[offset]) : "a" (pixel_color) ); __asm__ ( "\n\t\t fory:" "\n\t\t cmpl $255,%%eax" "\n\t\t jle bodyy" "\n\t\t jmp nexty" "\n\t\t bodyy:" __asm__ ( "\n\t\t incl %%eax" "\n\t\t jmp fory" "\n\t\t nexty:" :"=a" (y) :"a" (y) ); ); :"=a" (y) :"a" (y) __asm__ ( "\n\t\t\t pushl %%eax " "\n\t\t\t pushl $0 " "\n\t\t\t pushl %%ecx " "\n\t\t\t pushl %%edx " "\n\t\t\t call CreateHicolorPixel " "\n\t\t\t addl $16,%%esp " : "=a" (pixel_color) : "a" (y), "c" (x), "d" (screen->format) ); __asm__ ( "\n\t\t\t shrl %%eax " "\n\t\t\t movzwl %%ax ,%%eax " "\n\t\t\t imull %%ecx,%%eax " "\n\t\t\t addl %%edx,%%eax " : "=a" (offset) : "a" (screen->pitch) , "c" (y), "d" (x) ); __asm__ ( "\n\t\t incl %%eax" "\n\t\t jmp forx" "\n\t\t nextx:" :"=a" (x) :"a" (x) ); __asm__ ( "\n\t pushl %%eax " "\n\t call SDL_UnlockSurface " "\n\t popl %%eax " : ); __asm__ ( "\n\t pushl $0 " "\n\t pushl %%eax " "\n\t call SDL_UpdateRect " "\n\t addl $20,%%esp " : : "a" (screen) ); __asm__ ( "\n\t pushl $3000 " "\n\t call SDL_Delay " "\n\t popl %%eax " : ); return 0 ; } .text #1 #************************************** # # CreateHicolorPixel # #************************************** .globl CreateHicolorPixel .type CreateHicolorPixel, @function CreateHicolorPixel: pushl %ebp movl %esp, %ebp pushl %ebx subl $8, %esp movl 12(%ebp), %eax movl 16(%ebp), %edx movl 20(%ebp), %ecx movb %al, -5(%ebp) movb %dl, -6(%ebp) movb %cl, -7(%ebp) movzbl -5(%ebp), %edx movl 8(%ebp), %eax movzbl 6(%eax), %ecx sarl %cl , %edx movl 8(%ebp), %eax movzbl 10(%eax), %ecx movl %edx , %ebx sall %cl , %ebx movzbl -6(%ebp), %edx movl 8(%ebp), %eax movzbl 7(%eax), %ecx sarl %cl, %edx movl 8(%ebp), %eax movzbl 11(%eax), %ecx movl %edx, %eax sall %cl, %eax movl %ebx, %edx leal (%eax,%edx), %ebx movzbl -7(%ebp), %edx movl 8(%ebp), %eax movzbl 8(%eax), %ecx sarl %cl, %edx movl 8(%ebp), %eax movzbl 12(%eax), %ecx movl %edx, %eax sall %cl, %eax movl %ebx, %edx leal (%eax,%edx), %eax movw %ax, -10(%ebp) movzwl -10(%ebp), %eax addl $8, %esp popl %ebx popl %ebp ret #2 #*********************************** # main #************************************ .globl main .type main: pushl movl pushl subl andl movl subl main, @function %ebp %esp, %ebp %ebx $36, %esp $-16, %esp $0, %eax %eax, %esp pushl $32 call SDL_Init popl %eax movl $SDL_Quit, %eax pushl %eax call atexit popl %eax movl $SDL_Quit, %eax pushl $0 pushl $16 pushl $256 pushl $256 call SDL_SetVideoMode addl $16,%esp movl %eax, -8(%ebp) movl -8(%ebp), %eax pushl %eax call SDL_LockSurface popl %eax movl movl movl movl -8(%ebp), %eax 20(%eax), %eax %eax, -12(%ebp) -16(%ebp), %eax movl $0 ,%eax movl %eax, -16(%ebp) movl -16(%ebp), %eax forx: cmpl $255,%eax jle bodyx jmp nextx #3 bodyx: movl %eax, -16(%ebp) movl -20(%ebp), %eax movl $0 ,%eax movl %eax, -20(%ebp) movl -20(%ebp), %eax fory: cmpl $255,%eax jle bodyy jmp nexty movl movl movl movl movl movl pushl pushl pushl pushl bodyy: %eax, -20(%ebp) -20(%ebp), %ebx -16(%ebp), %ecx -8(%ebp), %eax 4(%eax), %edx %ebx, %eax %eax $0 %ecx %edx call CreateHicolorPixel addl $16,%esp movw %ax , -30(%ebp) movzwl -30(%ebp), %eax movw %ax , -22(%ebp) movl -8(%ebp) , %eax movzwl 16(%eax) , %eax movl -20(%ebp), %ecx movl -16(%ebp), %edx shrl %eax movzwl %ax ,%eax imull %ecx,%eax addl %edx,%eax movl %eax movzwl movw %ax nextx: , -28(%ebp) -22(%ebp), %eax , -30(%ebp) movl movl %eax, -16(%ebp) -8(%ebp), %eax #4 pushl %eax call SDL_UnlockSurface popl %eax movl -8(%ebp), %eax pushl $0 pushl $0 pushl $0 pushl $0 pushl %eax call SDL_UpdateRect addl $20,%esp pushl $3000 call SDL_Delay popl %eax movl $0, %eax movl -4(%ebp), %ebx leave ret CAPITOLO 18 Musica Maestro ! Musica Maestro ! Per quanto riguarda la grafica, ci sono numerose librerie, funzioni e formati grafici, non di meno per quanto riguarda la scienza della musica tra mixing, playback e format conversion. Il suono dei computer è basato sul PCM, (Pulse-code modulation). Se vogliamo fare un paragone, ogni pixel in un formato grafico rappresenta una parte dell'immagine, paragonato al formato PCM ogni pixel rappresenta l'intensità media senqueziale delle onde sonore. Nel nostro caso ogni pixel, viene chiamato sample. 'Sampling rate' viene espresso nello standard “SI frequency unit”, Hertz (Hz). Quindi un valore più grande del “sampling rate” permette una codifica più vicina al suono originale” PCM samples solitamente sono 8 o 16 bit (1 o 2 byte) per ogni canale) 1 canale per mono 2 per stereo, e per quanto riguarda la qualità dei giochi il range varia dai 22050 ai 44100 Hz. Il sample uò essere rappresentato come 'signed' or 'unsigned'. Per farvi un esempio delle dimensioni a 44100hz ottima qualità con codifica 16bit, 1 secondo equivale a 90 KB Questa tabella mostra il consumo in byte riferito ai bit e alla sua codifica Mono 8 bit Stero 16 bit 8 bit 16 bit 11025 Hz 11025 KB 22050 KB 22050 KB 44100 KB 22050 Hz 22050 KB 44100 KB 44100 KB 88200 KB 44100 Hz 44100 KB 88200 KB 88200 KB 176400 KB Il formato classico per memorizzare i file immagine è il BITMAP, il formato PCM solitamente vine memorizzato in formato WAV. SDL_LoadWAV ( file, spec, buffer, lentgh ) ; SDL_FreeWav ( buffer ) ; SDL_AuddioSpec Scheda Sonora Concettualmente una scheda sonora è semplice... Questa accetta di continui dei “samples PCM ” e ricrea il suono originale attraverso un set di casse. Per esempio vogliamo riprodurre la qualità di un CD (44.1KHz) il suono che formiremo alla scheda sonora sarà di 44100 samples per secondo. Con 2 canali per il suono stereo dovremo fornire l'equivalemte di 176400 byte/s. Sarebbe sconveniente interrompere il gioco 44 migliaia di volte al secondo per caricare i dati, oppure in computer lenti ci sarebbere perdite di suono. Fortunatamente i modermi computer includono una modalità chiamata DMA (Direct Memory Access), DMA fornisce il supporto per il trasferimento in background dei dati alla memoria al altà velocità. Quindi questo metodo anche se usato per una varietà di altre situazioni, ritorna utile per il trasferimento dei dati dall'hard disk alla scheda sonora. Periodicamente sarà il controller del DMA che ci dirà che è possibile spedire un'altro pacchetto dei dati, semplificandoci il lavoro. Non preoccupatevi in quanto il sistema operativo si occuperà di questo, per noi. L'unica cosa che dobbiamo preoccuparci di fornire i dati per il trasferimento corretti. All'atto pratico costruiremo una funzione “CallBack”, e quando verranno richiesti ulteriori dati, dall'hardware del compuer, SDL chiamerà esplicitamente questa funzione. La funzione di callback deve copiare velocemente i dati rigurdanti il suono in un dato buffer. (al caso nostro potremmo utilizzare le funzioni MMX per il trasferimento parallelo dei dati). Attenzione, questo metodo però ci riserva un problema! tutti i dati che inviamo alla scheda sonoro vengono elaborati sequenzialemente, questo mi va bene per una colonna sonora di sottofondo ma se devo reagire a un un tasto che indica uno sparo ?! Questo problema è chiamato “Latenza”, e noi dovremmo minimizzarlo quanto possibile. E' possibile minimizzare la latenza specificando un piccolo buffer, quando inizializzeremo la scheda sonora, ma non è possibile eliminare la latenza. Tutto sommato questo problema rispecchia la realtà in quanto la luce viaggia notevolmente più veloce che il suono, quindi prima avremo l'immagine poi con una piccola latenza udiremo il suono. (lampo e tuono). Prima di procedere dovete installare sul sitema un mixer, io l'ho prelevato dal sito originale delle SDL (http://www.libsdl.org/index.php) il file si tratta di : SDL_Mixer-1.2.6, scompattatelo nella directory home della vostra macchina eseguite le istruzioni di installazione : ./configure make make install. Se avrete qualche problema relative alle shared library, copiatele manualmente in /usr/lib, per default SDL li mette in /usr/local/lib ed accertatevi di fornire il percorso corretto degli header. Il prossimo programma mostra l'utilizzo di un file inteso come colonna sonora che possiamo avviare col tasto 'M' e di tre effetti speciali con i tasti '1' '2' '3' ancora i tasti freaccia aumentano / diminuiscono il volume e le casse laterali. come per la parte grafica presenterò il programma nella sua interezza in 'C' e passo per passo lo trasformo in assembler. #include <stdlib.h> #include </usr/include/SDL/SDL.h> #include </usr/local/include/SDL/SDL_mixer.h> void handleKey(SDL_KeyboardEvent key); Mix_Music *music; Mix_Chunk *sample1, *sample2, *sample3; int main(int argc, char **argv) { SDL_Surface *screen; SDL_Event event; Uint8 *keys; int quit = 0; int frequency = 44100; Uint16 format = MIX_DEFAULT_FORMAT; /* Corrisponde a AUDIO_S16SYS */ int channels = MIX_DEFAULT_CHANNELS; /* Stereo */ int buffers = 2048; Uint8 left = 127; int volume= MIX_MAX_VOLUME; char *music_file, *sample1_file, *sample2_file, *sample3_file; /* Operiamo il parsing dei parametri dati da shell */ if (argc == 1) { music_file = "music.ogg"; sample1_file = "sample1.ogg"; sample2_file = "sample2.ogg"; sample3_file = "sample3.ogg"; } else if (argc == 5) { music_file = argv[1]; sample1_file = argv[2]; sample2_file = argv[3]; sample3_file = argv[4]; } else { printf("La sintassi corretta è: %s musica camp1 camp2 camp3\n",argv[0]); exit(-1); } /* Inizializzazione del sottosistema audio e video */ if(SDL_Init(SDL_INIT_VIDEO|SDL_INIT_AUDIO) < 0) { printf("Errore init SDL: %s\n", SDL_GetError()); exit (-1); } /* All'uscita del programma viene eseguito SDL_Quit per risistemare le cose */ atexit(SDL_Quit); /* Apriamo una finestra per catturare gli eventi dell'utente */ if ((screen = SDL_SetVideoMode(320, 240, 0, 0)) == NULL) { printf("Errore apertura video: %s\n", SDL_GetError()); exit (-1); } /* Apriamo il dispositivo audio */ if (Mix_OpenAudio(frequency, format, channels, buffers) == -1) { printf("Errore apertura dispositivo audio: %s\n", Mix_GetError()); exit(-1); } /* Ridimensioniamo il numero di canali allocati, ce ne bastano solo 4 */ Mix_AllocateChannels(4); /* Analizziamo cosa ci ha effettivamente dato a disposizione il sistema */ int numtimesopened; numtimesopened = Mix_QuerySpec(&frequency, &format, &channels); if (!numtimesopened) printf("Errore con la Mix_QuerySpec(): %s\n", Mix_GetError()); else { char *format_str="Sconosciuto"; switch (format) { case AUDIO_U8: format_str="U8"; break; case AUDIO_S8: format_str="S8"; break; case AUDIO_U16LSB: format_str="U16LSB"; break; case AUDIO_S16LSB: format_str="S16LSB"; break; case AUDIO_U16MSB: format_str="U16MSB"; break; case AUDIO_S16MSB: format_str="S16MSB"; break; } printf("Il dispositivo è stato aperto %d volte\n", numtimesopened); printf("Specifiche audio ottenute dal sistema:\n"); printf("Frequenza = %d\n", frequency); printf("Formato = %s\n", format_str); printf("Canali = %d\n\n", channels); } /* Carichiamo la musica */ if ((music = Mix_LoadMUS(music_file)) == NULL) printf("Errore caricamento musica : %s\n", Mix_GetError()); /* Carichiamo i campioni audio */ if ((sample1 = Mix_LoadWAV(sample1_file)) == NULL) printf("Errore caricamento campione 1: %s\n", Mix_GetError()); if ((sample2 = Mix_LoadWAV(sample2_file)) == NULL) printf("Errore caricamento campione 2: %s\n", Mix_GetError()); if ((sample3 = Mix_LoadWAV(sample3_file)) == NULL) printf("Errore caricamento campione 3: %s\n", Mix_GetError()); /* Registriamo un effetto di panning centrale */ Mix_SetPanning(MIX_CHANNEL_POST, 127, 127); /* Ciclo di gestione degli eventi dell'utente */ while (!quit) { while (SDL_PollEvent(&event)) { switch (event.type) { case SDL_QUIT: quit = 1; break; case SDL_KEYDOWN: handleKey(event.key); break; } if (event.key.keysym.sym == SDLK_ESCAPE || event.key.keysym.sym == SDLK_q) quit = 1; } keys = SDL_GetKeyState(NULL); if (keys[SDLK_UP]) { if (volume < MIX_MAX_VOLUME) { volume += 4; Mix_Volume(-1, volume); Mix_VolumeMusic(volume); printf("Volume = %d\n", volume); } } if (keys[SDLK_DOWN]) { if (volume > 0) { volume -= 4; Mix_Volume(-1, volume); Mix_VolumeMusic(volume); printf("Volume = %d\n", volume); } } if (keys[SDLK_LEFT]) { if (left < 250) { left += 4; Mix_SetPanning(MIX_CHANNEL_POST, left, 254-left); printf("Panning = L:%d, R:%d\n", left, 254-left); } } if (keys[SDLK_RIGHT]) { if (left > 4) { left -= 4; Mix_SetPanning(MIX_CHANNEL_POST, left, 254-left); printf("Panning = L:%d, R:%d\n", left, 254-left); } } } /* Facciamo respirare la CPU tra un poll ed un altro */ SDL_Delay(50); /* Liberiamo la memoria allocata per i campioni e la musica e chiudiamo il dispositivo audio */ Mix_FreeChunk(sample3); Mix_FreeChunk(sample2); Mix_FreeChunk(sample1); Mix_FreeMusic(music); Mix_CloseAudio(); SDL_Quit(); } return 0; /*gestisce la struttura SDL_KeyboardEvent per rispondere all'input da tastiera */ void handleKey(SDL_KeyboardEvent key) { switch(key.keysym.sym) { case SDLK_c: Mix_SetPanning(MIX_CHANNEL_POST, 127, 127); printf("Panning = Centrale\n"); break; case SDLK_f: if (Mix_PlayingMusic()) Mix_FadeOutMusic(3000); else Mix_FadeInMusic(music, -1, 3000); break; case SDLK_m: if (Mix_PlayingMusic()) Mix_HaltMusic(); else Mix_PlayMusic(music, -1); break; case SDLK_p: if (Mix_PausedMusic()) Mix_ResumeMusic(); else Mix_PauseMusic(); break; case SDLK_1: case SDLK_KP1: Mix_PlayChannel(1, sample1, 0); break; case SDLK_2: case SDLK_KP2: Mix_PlayChannel(2, sample2, 0); break; case SDLK_3: case SDLK_KP3: Mix_PlayChannel(3, sample3, 0); break; } } Questo è il programma in C, gentilmente scopiazzato per l'occasione da internet. Seguite questi passi di compilazione per arrivare comodamente al codice in assembler e quindi linkarlo e modificare l'assembly direttamente senza usare l'inline. Considesiamo il programma si chiami audio.c : audio.c : con questo passaggio si arriva ad produrre il sorgente in assembly gcc -I/usr/include/SDL -D_REENTRANT \ -L/usr/lib -lSDL -lpthread -lSDL_mixer \ -S $1.c -o $1.s audio.s : ora è la volta del codice oggetto as –gstabs+ audio.s -o audio.o audio.bin : e ora linkiamo tutto insieme gcc -I/usr/include/SDL -D_REENTRANT \ -L/usr/lib -lSDL -lpthread -lSDL_mixer \ $1.o -o $1.bin Ora da qui in poi possiamo lavorare comodamente sul file audio.s e linkare solamente il codice oggetto con tutte le variabile e i valori di inclusione corretti ! Vediamo il risultato ! .file .section "audio7.c" .rodata ############################################# DEFAULT FILE # ############################################# STR_MUSIC_OGG: .string "music.ogg" STR_SAMPLE1: .string "sample1.ogg" STR_SAMPLE2: .string "sample2.ogg" STR_SAMPLE3: .string "sample3.ogg" ############################################ MESSAGGI STATO # ############################################ STR_U8: .string STR_S8: .string STR_U16LSB: .string STR_S16LSB: .string STR_U16MSB: .string STR_S16MSB: .string STR_VOL: .string STR_PAN: .string "U8" "S8" "U16LSB" "S16LSB" "U16MSB" "S16MSB" "Volume = %d\n" "Panning = L:%d, R:%d\n" ############################################ MESSAGGI DI ERRORE AUDIO/VIDEO # ############################################ .align 32 STR_ERR_01: .string "La sintassi corretta \350: %s musica campione1 campione2 campione3\n" STR_ERR_02: .string "Errore init SDL: %s\n" STR_ERR_03: .string "Errore apertura video: %s\n" .align 32 STR_ERR_04: .string "Errore apertura dispositivo audio: %s\n" .align 32 STR_ERR_05: .string "Errore con la Mix_QuerySpec(): %s\n" STR_ERR_00: .string "Sconosciuto" ############################################ MESSAGGI STATO # ############################################ .align 32 STR_DISP_OPEN: .string "Il dispositivo \350 stato aperto %d volte\n" .align 32 STR_SPEC: .string "Specifiche audio ottenute dal sistema:\n" STR_FREQ: .string "Frequenza = %d\n" STR_FORMATO: .string "Formato = %s\n" STR_CANALI: .string "Canali = %d\n\n" ############################################ MESSAGGI DI ERRORE FILE # ############################################ .align 32 STR_ERR_06: .string "Errore caricamento musica : %s\n" STR_RB: .string "rb" .align 32 STR_ERR_07: .string "Errore caricamento campione 1: %s\n" .align 32 STR_ERR_08: .string "Errore caricamento campione 2: %s\n" .align 32 STR_ERR_09: .string "Errore caricamento campione 3: %s\n" ############################################ MAIN # ############################################ .text .globl main .type main: pushl movl subl andl movl subl main, @function %ebp %esp $152 $-16 $0 %eax, , %ebp , %esp , %esp , %eax %esp .equ .equ .equ .equ .equ .equ .equ buffers, quit freq channels format left volume, -80 , , , , , -88 .equ .equ MIX_MAX_VOLUME ,128 MIX_DEF_FORMAT ,-32752 movl movl movw movl movl movb movl $0 $44100 $MIX_DEF_FORMAT $2 $2048 $127 $MIX_MAX_VOLUME -64 -68 -76 -70 -81 , , , , , , , quit(%ebp) freq(%ebp) format(%ebp) channels(%ebp) buffers(%ebp) left(%ebp) volume(%ebp) ############################################ # operiamo il parsing dei parametri da shell ############################################ .equ .equ .equ .equ .equ argc music_file sample1_file sample2_file sample3_file , , , , , cmpl jne $1, argc(%ebp) ENDIF_ARGC_EQ_1 movl movl movl movl jmp $STR_MUSIC_OGG $STR_SAMPLE1 $STR_SAMPLE2 $STR_SAMPLE3 .L3 8 -92 -96 -100 -104 , , , , music_file(%ebp) sample1_file(%ebp) sample2_file(%ebp) sample3_file(%ebp) ENDIF_ARGC_EQ_1: .L4: .equ argv1 , 12 cmpl jne movl addl movl movl movl addl movl movl movl addl movl movl movl addl movl movl jmp $5 .L4 argv1(%ebp) $4 (%eax) , %eax %eax argv1(%ebp) $8 (%eax) , %eax %eax argv1(%ebp) $12 (%eax) , %eax %eax argv1(%ebp) $16 (%eax) , %eax %eax .L3 , argc(%ebp) movl movl movl movl call movl call argv1(%ebp) (%eax) %eax $STR_ERR_01 printf $-1 exit , %eax , %eax , music_file(%ebp) , %eax , %eax , sample1_file(%ebp) , %eax , %eax , sample2_file(%ebp) , %eax , %eax , sample3_file(%ebp) , %eax , %eax , 4(%esp) , (%esp) , (%esp) ############################################ # inizializzazione sottostitema audio/video ############################################ .L3: .L6: .equ INIT_AUDIO_VIDEO , movl call testl jns call movl movl call movl call $INIT_AUDIO_VIDEO , SDL_Init %eax , .L6 SDL_GetError %eax , $STR_ERR_02 , (%esp) printf $-1 , exit movl call $SDL_Quit atexit 48 (%esp) %eax 4(%esp) (%esp) , (%esp) ###################################################### # apriamo una finestra per catturare gli eventi utente ###################################################### movl movl movl movl call $0 , 12(%esp) $0 , 8(%esp) $240 , 4(%esp) $320 , (%esp) SDL_SetVideoMode .equ screen , movl cmpl jne call %eax , screen(%ebp) $0 , screen(%ebp) .L7 SDL_GetError movl movl call movl call %eax , 4(%esp) $STR_ERR_03 , (%esp) printf $-1 , (%esp) exit -12 ############################################ # Apriamo il dispositivo audio ############################################ .L7: movl buffers(%ebp) , %eax movl %eax movl channels(%ebp) movl %eax movzwl format(%ebp), movl %eax movl freq(%ebp) movl %eax call Mix_OpenAudio cmpl jne call movl movl call movl call , 12(%esp) , %eax , 8(%esp) %eax , 4(%esp) , %eax , (%esp) $-1 , %eax .L8 SDL_GetError %eax , 4(%esp) $STR_ERR_04 , (%esp) printf $-1 , (%esp) exit ############################################ # Ridimensioniamo canali allocati (MAX 4) ############################################ .L8: .equ MAX_CHANNELS , movl call $MAX_CHANNELS , (%esp) Mix_AllocateChannels leal movl leal movl leal movl call channels(%ebp) %eax format(%ebp) %eax freq(%ebp) %eax Mix_QuerySpec , , , , , , 4 %eax 8(%esp) %eax 4(%esp) %eax (%esp) ############################################ # Cosa effettivamente ci ha dato il sistema? ############################################ .L9: .equ num_times_opened,-108 movl %eax , num_times_opened(%ebp) cmpl jne $0 .L9 , num_times_opened(%ebp) call movl movl call jmp SDL_GetError %eax , 4(%esp) $STR_ERR_05, (%esp) printf .L10 .equ .equ str_format loc_format movl movzwl movl , , -112 -116 $STR_ERR_00 , str_format(%ebp) format(%ebp) , %eax %eax , loc_format(%ebp) #################################################### SWITCH .L20: .L21: .L12: .L13: .L14: .L15: .L16: .L17: cmpl je cmpl jg cmpl je cmpl je jmp $4112 .L16 $4112 .L20 $8 .L12 $16 .L14 .L11 , loc_format(%ebp) cmpl je cmpl jg cmpl je jmp $32784 .L15 $32784 .L21 $32776 .L13 .L11 , loc_format(%ebp) cmpl je jmp $36880 .L17 .L11 , loc_format(%ebp) movl jmp $STR_U8 .L11 , str_format(%ebp) movl jmp $STR_S8 .L11 , str_format(%ebp) movl jmp $STR_U16LSB, str_format(%ebp) .L11 movl jmp $STR_S16LSB, str_format(%ebp) .L11 movl jmp $STR_U16MSB, str_format(%ebp) .L11 movl $STR_S16MSB, str_format(%ebp) , loc_format(%ebp) , loc_format(%ebp) , loc_format(%ebp) , loc_format(%ebp) , loc_format(%ebp) .L11: ############################################# # # Vis. Num. volte Dispositivo Aperto # ############################################# movl movl movl call num_times_opened(%ebp) %eax $STR_DISP_OPEN printf , %eax , 4(%esp) , (%esp) ############################################# Vis. Specifiche movl call $STR_SPEC, (%esp) printf ############################################# Vis. Frequenza movl movl movl call freq(%ebp) %eax $STR_FREQ printf , %eax , 4(%esp) , (%esp) ############################################# Vis. Formato movl movl movl call str_format(%ebp) %eax $STR_FORMATO printf , %eax , 4(%esp) , (%esp) ############################################# Vis. Canali movl movl movl call .L10: channels(%ebp) %eax $STR_CANALI printf , %eax , 4(%esp) , (%esp) ############################################### Carichiamo la musica # ############################################### movl movl call music_file(%ebp) %eax Mix_LoadMUS , %eax , (%esp) movl cmpl jne call %eax, music $0, music .L22 SDL_GetError movl movl call %eax , 4(%esp) $STR_ERR_06 , (%esp) printf .L22: ################################## load sample1 movl movl movl call $STR_RB sample1_file(%ebp) %eax SDL_RWFromFile , 4(%esp) , %eax , (%esp) movl movl call $1 , 4(%esp) %eax , (%esp) Mix_LoadWAV_RW movl cmpl jne call %eax, sample1 $0, sample1 .L23 SDL_GetError movl movl call %eax , 4(%esp) $STR_ERR_07 , (%esp) printf ################################## load sample2 .L23: movl movl movl call $STR_RB sample2_file(%ebp) %eax SDL_RWFromFile , 4(%esp) , %eax , (%esp) movl movl call $1 , 4(%esp) %eax , (%esp) Mix_LoadWAV_RW movl cmpl jne call %eax , sample2 $0 , sample2 .L24 SDL_GetError movl movl call %eax , 4(%esp) $STR_ERR_08 , (%esp) printf .L24: .L25: .L26: .L29: .L31: ################################## load sample3 movl movl movl call $STR_RB sample3_file(%ebp) %eax SDL_RWFromFile , 4(%esp) , %eax , (%esp) movl movl call $1 , 4(%esp) %eax , (%esp) Mix_LoadWAV_RW movl cmpl jne call %eax , sample3 $0 , sample3 .L25 SDL_GetError movl movl call %eax , 4(%esp) $STR_ERR_09 , (%esp) printf ############################################ # # Registriamo un effetto di panning centrale # ############################################ .equ MIX_CHANNEL_POST movl movl movl call , -2 $127 , 8(%esp) $127 , 4(%esp) $-2 , (%esp) Mix_SetPanning # WHILE (!QUIT) cmpl je jmp $0 .L29 .L27 leal movl call -56(%ebp) , %eax %eax , (%esp) SDL_PollEvent testl jne jmp %eax .L31 .L30 movzbl-56(%ebp) movl %eax cmpl $2 je .L34 cmpl $12 je .L33 jmp .L32 , quit(%ebp) , %eax , %eax , -120(%ebp) , -120(%ebp) , -120(%ebp) .L33: .L34: .L32: .L38: .L30: movl jmp $1, quit(%ebp) .L32 movl movl movl movl movl movl movl movl movl movl call -56(%ebp) %eax -52(%ebp) %eax -48(%ebp) %eax -44(%ebp) %eax -40(%ebp) %eax handleKey , , , , , , , , , , cmpl je cmpl je jmp $27 .L38 $113 .L38 .L29 , -48(%ebp) movl jmp $1, quit(%ebp) .L29 movl call $0 , (%esp) SDL_GetKeyState .equ keys , -60 movl movl addl cmpb je %eax keys(%ebp) $273 $0 .L39 , , , , cmpl jg $127 .L39 , volume(%ebp) leal addl movl volume(%ebp) , %eax $4, (%eax) volume(%ebp) , %eax movl movl call %eax $-1 Mix_Volume movl movl call volume(%ebp) , %eax %eax , (%esp) Mix_VolumeMusic movl movl movl call volume(%ebp) , %eax %eax , 4(%esp) $STR_VOL , (%esp) printf %eax (%esp) %eax 4(%esp) %eax 8(%esp) %eax 12(%esp) %eax 16(%esp) , -48(%ebp) keys(%ebp) %eax %eax (%eax) , 4(%esp) , (%esp) .L39: .L41: movl addl cmpb je cmpl jle keys(%ebp) $274 $0 .L41 $0 .L41 , %eax , %eax , (%eax) leal subl movl volume(%ebp) , %eax $4 , (%eax) volume(%ebp) , %eax movl movl call %eax $-1 Mix_Volume movl movl call volume(%ebp) , %eax %eax , (%esp) Mix_VolumeMusic movl movl movl call volume(%ebp) , %eax %eax , 4(%esp) $STR_VOL , (%esp) printf movl addl cmpb je cmpb ja keys(%ebp) $276 $0 .L43 $-7 .L43 , volume(%ebp) , 4(%esp) , (%esp) , %eax , %eax , (%eax) , left(%ebp) leal left(%ebp) , %eax addb $4 , (%eax) movb $-2 , %al subb left(%ebp) , %al movzbl%al , %eax movl %eax , 8(%esp) movzblleft(%ebp) , %eax movl %eax , 4(%esp) movl $-2 , (%esp) call Mix_SetPanning movzblleft(%ebp) movl $254 subl %edx movl %eax movzblleft(%ebp) movl movl call %eax $STR_PAN printf , %edx , %eax , %eax , %eax , 8(%esp) , 4(%esp) , (%esp) .L43: movl keys(%ebp) , %eax addl $275 , %eax cmpb $0 , (%eax) je .L45 cmpb $4 , left(%ebp) jbe .L45 leal left(%ebp) , %eax subb $4 , (%eax) movb $-2 , %al subb left(%ebp) , %al movzbl %al , %eax movl %eax , 8(%esp) movzbl left(%ebp) , %eax movl %eax , 4(%esp) movl $-2 , (%esp) call Mix_SetPanning movzbl left(%ebp) movl $254 subl %edx movl %eax movzbl left(%ebp) movl %eax movl $STR_PAN call printf .L45: , %edx , %eax , %eax , 8(%esp) , %eax , 4(%esp) , (%esp) ################################################## # facciamo respirare la cpu tra un poll e un altro ################################################## movl call jmp $50, (%esp) SDL_Delay .L26 .L27: .LC28: ##################################### THE END # ##################################### movl movl call sample3 , %eax %eax , (%esp) Mix_FreeChunk movl movl call sample2 , %eax %eax , (%esp) Mix_FreeChunk movl movl call sample1 , %eax %eax , (%esp) Mix_FreeChunk movl movl call music , %eax %eax , (%esp) Mix_FreeMusic call Mix_CloseAudio call SDL_Quit movl $0, %eax leave ret .size main, .-main .section .rodata .string "Panning = Centrale\n" ############################################## HANDLEKEY # ############################################## .text .globl handleKey .type handleKey, @function handleKey: pushl %ebp movl %esp, %ebp subl $24, %esp .equ .equ KEY_KEYSYM_SYM key movl key(%ebp), %eax , , -4 16 .L68: .L67: .L69: movl cmpl je %eax, KEY_KEYSYM_SYM(%ebp) $102, KEY_KEYSYM_SYM(%ebp) .L50 cmpl ja $102, KEY_KEYSYM_SYM(%ebp) .L67 cmpl je $50, KEY_KEYSYM_SYM(%ebp) .L62 cmpl ja $50, KEY_KEYSYM_SYM(%ebp) .L68 cmpl je $49, KEY_KEYSYM_SYM(%ebp) .L60 jmp .L47 cmpl je $51, KEY_KEYSYM_SYM(%ebp) .L64 cmpl je $99, KEY_KEYSYM_SYM(%ebp) .L49 jmp .L47 cmpl je $257, KEY_KEYSYM_SYM(%ebp) .L60 cmpl ja $257, KEY_KEYSYM_SYM(%ebp) .L69 cmpl je $109, KEY_KEYSYM_SYM(%ebp) .L53 cmpl je $112, KEY_KEYSYM_SYM(%ebp) .L56 jmp .L47 cmpl je $258, KEY_KEYSYM_SYM(%ebp) .L62 cmpl je $259, KEY_KEYSYM_SYM(%ebp) .L64 jmp .L47 .L49: .L50: .L51: .L53: .L54: .L56: .L57: ############################### case c movl movl movl call movl call jmp $127 , 8(%esp) $127 , 4(%esp) $-2 , (%esp) Mix_SetPanning $.LC28 , (%esp) printf .L47 ############################### case f call testl je movl call jmp Mix_PlayingMusic %eax , %eax .L51 $3000 , (%esp) Mix_FadeOutMusic .L47 movl movl movl movl call jmp $3000 , 8(%esp) $-1 , 4(%esp) music , %eax %eax , (%esp) Mix_FadeInMusic .L47 ############################### case m call testl je call jmp Mix_PlayingMusic %eax , %eax .L54 Mix_HaltMusic .L47 movl movl movl call jmp $-1 , 4(%esp) music , %eax %eax , (%esp) Mix_PlayMusic .L47 ################################ Case p call testl je call jmp Mix_PausedMusic %eax , %eax .L57 Mix_ResumeMusic .L47 call jmp Mix_PauseMusic .L47 .L60: .L62: .L64: .L47: ################################# CASE 1 movl movl movl movl movl call jmp $-1 , 12(%esp) $0 , 8(%esp) sample1 , %eax %eax , 4(%esp) $1 , (%esp) Mix_PlayChannelTimed .L47 ################################# CASE 2 movl movl movl movl movl call jmp $-1 , 12(%esp) $0 , 8(%esp) sample2 , %eax %eax , 4(%esp) $2 , (%esp) Mix_PlayChannelTimed .L47 ################################# CASE 3 movl movl movl movl movl call $-1 , 12(%esp) $0 , 8(%esp) sample3 , %eax %eax , 4(%esp) $3 , (%esp) Mix_PlayChannelTimed leave ret .size handleKey, .-handleKey .comm music .comm sample1 .comm sample2 .comm sample3 ,4 ,4 ,4 ,4 ,4 ,4 ,4 ,4 # offset, dimensione byte,allineamento .section .note.GNU-stack,"",@progbits .ident "GCC: (GNU) 3.3.5 (Debian 1:3.3.5-13)" CAPITOLO 19 GNOME / GTK GNOME / GTK Questo capitolo vi introduce all'utilizzo di Gnome, in particolare della libreria gtk, nel modo più semplice e veloce possibile. Non vengono presi in considerazioni i dettagli ma viene fornito uno start-up al lettore che voglia approfondire l'argomento, come del resto anche per i restanti capitoli. La programmazione di Gnome come ambiente desktop si basa sulle GTK , una caratteristica molto interessante di queste librerie è il lavoro minimo che deve fare il programmatore nel gestire le strutture di dati allocate anche molto complesse, in quanto la libreria fornisce apposiste funzioni per manipolare i dati. Questo metodo sicuramente è ottimo e ci fa capire la bontà con cui è stato programmato Gnome, pensate di dover cambiare anche solo alcuni dati alle vostre strutture, non dovrete perciò dover rivisitare il codice, piuttosto verranno modificati i dati attraverso le funzioni, perciò anche se hai a che fare con dei puntatori, ti interessa sapere solo il significato della funzione che usi. Il primo programma apre semplicemente una finestra sullo schermo, ulteriori dettagli vengono fornito attraverso i commetti a fianco delle istruzioni. Compilazione : Se da un listato c volete creae un in assembler : gcc -S prog.c -o progr.s 'pkg-config –cflags –libs gtk+-2.0' Se avete già un listato in assember as –gstabs+ progr.s -o progr.o gcc prog.o -o progr 'pkg-config –cflags –libs gtk+-2.0' E' risaputo che ottimizzando alcuni programmi al limite questi crashano, il mio consiglio è quello di utilizzare per quanto rigarda il cflags le ottimizzazione di base da (-00 a -03) ed evitare se non si è più che sicuri l'opzione -fomit-frame-pointer) Come installare GNOME Debian Se usate slink aggiungete in /etc/apt/sources.list: deb http://www.debian.org/~jim/debian-gtk-gnome/gnome-stage-slink unstable main Se invece state usando una distribuzione aggiornata a potato: deb http://www.debian.org/~jules/gnome-stage-2 unstable main Quindi, una volta connessi alla rete, dati questi comandi: # apt-get update # apt-get install gnome-panel gnome-session gnome-control-center gmc Link tutorial GTK ftp://ftp.gtk.org/pub/gtk/tutorial/ Primo Programma .equ .equ argc argv , , 8 12 .data window: .long 0 .text .globl main .type main: # alloca un puntatore alla finestra da visualizzare main, @function pushl %ebp movl %esp, %ebp , , , , %esp %esp %eax %esp # funzione principale per il compilatore GCC, se usi _start (vedi -e) # salva lo stack subl andl movl subl $24 $-16 $0 %eax # gtk_init ( &argc,&argv ) ; leal pushl leal pushl call addl argv(%ebp), %eax %eax argc(%ebp), %eax %eax gtk_init $8,%esp # gtk_window_new ( GTK_WINDOW_TOPLEVEL) pushl $0 call gtk_window_new addl $4,%esp # allinea i dati correttamente # inizializza la libreria gtk con i parametri da riga di comando # crea una finestra # window = gtk_window_new ( ... ) movl %eax, window # gtk_widget_show ( window ) ; movl pushl call addl window, %eax %eax gtk_widget_show $4,%esp # mostra la finestra indicata dal puntatore call gtk_main # lascia fare a Gnome # movl leave ret return 0 $0, %eax # termina # memorizza il puntatore 32-bit Hello World Nella documentazione relativa alle gtk, il programma “hello world”, mostra un esempio che fa uso di eventi del sistema, mi soffermerò su questa parte in quanto è molto importante. Per quanto riguarda la costruzione di interfacce,sconsiglierei vivamente di programmarle a mano, soprattutto in assembler, esistono diversi tool quali GLADE o ANJUTA, che semplificano notevolmente il lavoro, generando per voi un file di progetto, e controllando tutti gli eventi associati agli oggetti, poi prendendo un file in c/c++ generato, avete la possibilità se volete di convertirlo in assembler e giocarci un po' su. La libreria gtk, fa uso di macro quindi la traduzione dal c all'assembler a volte riserva qualche sopresa di fatti dapprima vi mostrerò il sorgente in C, e poi quello generato in assembler, troverete l'aggiunta di qualche funzione da voi non richiesta e ripetitiva al primo sguardo ma fondamentale per la programmazione in gtk. esempio in C : #include <gtk/gtk.h> /* funzioni di call back richiamati dagli eventi */ static void hello( GtkWidget *widget, gpointer data ) { g_print ("Hello World\n"); } static gboolean delete_event( GtkWidget *widget, GdkEvent *event, gpointer data ) { /* intercetta l'evento di chiusura oggetto */ g_print ("delete event occurred\n"); /* quando l'evento termina con true vogliamo che la finestra NON sia distrutta */ /* se mettiamo FALSE l'oggetto verrà distrutto é/ } return TRUE; static void destroy( GtkWidget *widget, gpointer data ) { gtk_main_quit (); } int main( int argc, { GtkWidget *window; GtkWidget *button; char *argv[] ) /* inizializza */ gtk_init (&argc, &argv); /* crea una finestra */ window = gtk_window_new (GTK_WINDOW_TOPLEVEL); /* connetti l'evento “delete_event” all'oggetto “windows” e chiama la funzione */ /* funzione : delete_event. NULL cioè non passargli parametri */ g_signal_connect (G_OBJECT (window), "delete_event", G_CALLBACK (delete_event), NULL); g_signal_connect (G_OBJECT (window), "destroy", G_CALLBACK (destroy), NULL); /* setta il bordo della finsetra */ gtk_container_set_border_width (GTK_CONTAINER (window), 10); /* crea un bottone con label “Hello World” */ button = gtk_button_new_with_label ("Hello World"); /* connetti l'oggetto “button” al segnale “clicked” e chiama la funzione hello */ g_signal_connect (G_OBJECT (button), "clicked", G_CALLBACK (hello), NULL); /* connetti un successivo segnale “clicked” all'oggetto “button” e “distruggilo” g_signal_connect_swapped (G_OBJECT (button), "clicked", G_CALLBACK (gtk_widget_destroy), G_OBJECT (window)); /* addiziona l'oggetto “button” al contenitore principale “window” gtk_container_add (GTK_CONTAINER (window), button); /* visualizza il bottone */ gtk_widget_show (button); /* visualizza la finestra */ gtk_widget_show (window); /* tutte le applicazioni devono terminare con gtk_main() */ gtk_main (); } return 0; Ora vediamo lo stesso programma generato in assembler : .file "secondo.c" .section .rodata.str1.1,"aMS",@progbits,1 str_delete_event: .string "delete_event" str_destroy: .string "destroy" str_hello_world: .string "Hello World" str_clicked: .string "clicked" ################################################ M A I N # ################################################ .text .p2align 4,,15 .globl main .type main, @function main: pushl %ebp movl %esp, %ebp .equ .equ argc argv , , 8 12 leal pushl leal pushl xorl pushl argv(%ebp), %edx %edi argc(%ebp), %edi %esi %esi, %esi %ebx ########################### variabili locali # GtkWidget *window; # GtkWidget *button; ########################### subl $28, %esp movl andl $80, %ebx $-16, %esp ########################### gtk_init (&argc, &argv); movl movl call %edx, 4(%esp) %edi, (%esp) gtk_init ########################### window = gtk_window_new (GTK_WINDOW_TOPLEVEL); movl $0, (%esp) call gtk_window_new movl movl movl movl call %eax, (%esp) %eax, %edi %ebx, 4(%esp) $delete_event, %ebx g_type_check_instance_cast ########################### # g_signal_connect ( # G_OBJECT (window), "delete_event", # G_CALLBACK (delete_event), NULL); ########################### movl %eax, (%esp) xorl %ecx, %ecx xorl %edx, %edx movl %ecx, 20(%esp) movl $str_delete_event, %ecx movl %edx, 12(%esp) movl %ecx, 4(%esp) movl %esi, 16(%esp) xorl %esi, %esi movl %ebx, 8(%esp) xorl %ebx, %ebx call g_signal_connect_data movl movl movl call %edi, (%esp) $80, %eax %eax, 4(%esp) g_type_check_instance_cast ########################### # g_signal_connect ( # G_OBJECT (window), "destroy", # G_CALLBACK (destroy), NULL); ########################### movl xorl movl movl movl movl movl movl movl xorl movl call %esi, 20(%esp) %edx, %edx $destroy, %ecx %edx, 12(%esp) $str_destroy, %esi %ecx, 8(%esp) %ebx, 16(%esp) $10, %ebx %esi, 4(%esp) %esi, %esi %eax, (%esp) g_signal_connect_data call gtk_container_get_type movl movl movl call %eax, -16(%ebp) %edi, (%esp) %eax, 4(%esp) g_type_check_instance_cast ################################### ################################### movl %eax, (%esp) movl %ebx, 4(%esp) call gtk_container_set_border_width gtk_container_set_border_width ( GTK_CONTAINER (window), 10); ################################### # button = gtk_button_new_with_label ("Hello World"); ################################### movl call $str_hello_world, (%esp) gtk_button_new_with_label movl movl movl movl call %eax, %ebx $80, %eax %ebx, (%esp) %eax, 4(%esp) g_type_check_instance_cast ################################## # g_signal_connect ( # G_OBJECT (button), "clicked", # G_CALLBACK (hello), NULL); ################################## movl xorl xorl movl movl movl movl movl movl movl movl call %esi, 16(%esp) %edx, %edx %ecx, %ecx %edx, 20(%esp) $80, %esi $hello, %edx %ecx, 12(%esp) $str_clicked, %ecx %edx, 8(%esp) %ecx, 4(%esp) %eax, (%esp) g_signal_connect_data movl movl call %esi, 4(%esp) %edi, (%esp) g_type_check_instance_cast movl movl movl movl call %ebx, (%esp) $80, %ecx %eax, %esi %ecx, 4(%esp) g_type_check_instance_cast ############################### # g_signal_connect_swapped ( # G_OBJECT (button), "clicked", # G_CALLBACK (gtk_widget_destroy), # G_OBJECT(window)); ############################### movl xorl movl movl movl movl movl movl movl movl call %eax, (%esp) %ecx, %ecx $2, %edx %edx, 20(%esp) $gtk_widget_destroy, %edx %ecx, 16(%esp) $str_clicked, %ecx %ecx, 4(%esp) %esi, 12(%esp) %edx, 8(%esp) g_signal_connect_data movl movl movl call %edi, (%esp) -16(%ebp), %edx %edx, 4(%esp) g_type_check_instance_cast ###################################### # gtk_container_add ( # GTK_CONTAINER (window), button); ##################################### movl %eax, (%esp) movl %ebx, 4(%esp) call gtk_container_add ###################################### gtk_widget_show (button); movl %ebx, (%esp) call gtk_widget_show ###################################### gtk_widget_show (window); movl %edi, (%esp) call gtk_widget_show ####################################### gtk_main (); call gtk_main ####################################### return 0 xorl %eax, %eax leal -12(%ebp), %esp .LC4: popl %ebx popl %esi popl %edi popl %ebp ret .size main, .-main .section .rodata.str1.1 .string "Hello World\n" ################################################ hello # ################################################ hello: .LC5: .text .p2align 4,,15 .type hello, @function pushl %ebp movl %esp, %ebp movl $.LC4, 8(%ebp) popl %ebp jmp g_print .size hello, .-hello .section .rodata.str1.1 .string "delete event occurred\n" ################################################ delete_event # ################################################ .text .p2align 4,,15 .type delete_event, @function delete_event: pushl %ebp movl %esp, %ebp subl $8, %esp movl $.LC5, (%esp) call g_print movl %ebp, %esp movl $1, %eax popl %ebp ret .size delete_event, .-delete_event ################################################ destroy # ################################################ .p2align 4,,15 .type destroy, @function destroy: pushl %ebp movl %esp, %ebp popl %ebp ########################################### QUIT jmp gtk_main_quit .size destroy, .-destroy .section .note.GNU-stack,"",@progbits .ident "GCC: (GNU) 3.3.5 (Debian 1:3.3.5-13)" Il programma è piuttosto autoesplicativo, se letto dalla parte C e commentato dalla parte in assembler. Come avete potuto notare compaiono delle funzioni nuove tipo : call g_type_check_instance_cast Alcune chiamate utilizzate nel linguaggio C, sono delle macro che vengono poi espanse benchè la programmazione in questo caso di interfacce grafiche viene limitata al solo passaggiodi parametri, sconsiglio vivamente ancora una volta di programmarle da zero, ed affidarsi a tool che semplifichino per noi il lavoro. Il listato è stato compilato con le ottimizzazioni O3 del gcc per poter far passare più variabili attraverso i registri. Notate tipico del C il passaggio dei valori viene fatto raramente con push , piuttosto con movl xxx,-4(%esp) incrementando il cntatore del size delle variabile passata e senza ripristinare ovviamente il registro %esp con addl $xxx,%esp. Elenco librerie linkate alla creazione dell'eseguibile : ● The GTK library (-lgtk), the widget library,basata su GDK. ● The GDK library (-lgdk), the Xlib wrapper. ● The gdk-pixbuf library (-lgdk_pixbuf), manipolazione di immagini. ● The Pango library (-lpango) testo internazionale. ● The gobject library (-lgobject), tipo di system su cui gtk è basato. ● The gmodule library (-lgmodule), utilizzato per le stensini run-time. ● The GLib library (-lglib), miscellanea ● The Xlib library (-lX11) usato da gtk. ● The Xext library (-lXext). codice per pixmaps extensions. ● The math library (-lm). utilizzo da gtk per scopi generali.. Teoria dei segnali e delle funzioni di CallBack Nella versione 2.0 i segnali sono stati cambiati da GTK a Glib, perciò le funzioni che andremo ad esaminare inizieranno con “g_” piuttosto che con “gtk_” come prefisso. GTK posiamo vederlo come un driver di eventi, significa che la libreria è in attesa nella funzione gtk_main() finchè non intercorre un evento e passa il controllo all'approppriata funzione. Il passaggio dei controlli viene effettuato mediante i 'segnali'. Quando un evento occorre per esempio “pressione di un tasto” un segnale approppriato viene emesso, dal widget attivo. Per far si che un oggetto possa compiere un'azione non dobbiamo connettere il segnale ad esso mediante : gulong g_signal_connect( gpointer *object, #1 const gchar *name, #2 GCallback func, #3 gpointer func_data ); #4 #1 widget che emette il segnale #2 tipo di segnale che si desidera catturare #3 funzione da attivare (callback function) #4 argomenti che desideriamo passare alla funzione void callback_function( GtkWidget *widget, ... /* other signal arguments */ gpointer callback_data ); #1 #2 #1 #2 puntatore al widget che ha emesso il segnale puntatore a dati passati alla funzione un'altra forma di callback function è la seguente : gulong g_signal_connect_swapped( gpointer const gchar GCallback gpointer #1 widget che emette il segnale #2 tipo di segnale che si d3esidera catturare #3 funzione da attivare (callback function) #4 argomenti che desideriamo passare alla funzione *object, #1 *name, #2 func, #3 *callback_data ); #4 Questa funzione è simile alla precedente eccetto per il fatto che gli argomenti della callback function sono inverti quindi la callback function avrà questa forma : void callback_func( gpointer callback_data, ... /* other signal arguments */ GtkWidget *widget); EVENTI GdkEvent button_press_event GDK_NOTHING button_release_event GDK_DELETE scroll_event GDK_DESTROY motion_notify_event GDK_EXPOSE delete_event GDK_MOTION_NOTIFY destroy_event GDK_BUTTON_PRES S expose_event GDK_2BUTTON_PRES S key_press_event GDK_3BUTTON_PRES S key_release_event GDK_BUTTON_RELEASE enter_notify_event GDK_KEY_PRES S leave_notify_event GDK_KEY_RELEASE configure_event GDK_ENTER_NOTIFY focus_in_event GDK_LEAVE_NOTIFY focus_out_event GDK_FOCUS_CHANGE map_event GDK_CONFIGURE unmap_event GDK_MAP property_notify_event GDK_UNMAP selection_clear_event GDK_PROPERTY_NOTIFY selection_request_event GDK_SELECTION_CLEAR selection_notify_event GDK_SELECTION_REQUEST proximity_in_event GDK_SELECTION_NOTIFY proximity_out_event GDK_PROXIMITY_IN visibility_notify_event GDK_PROXIMITY_OUT client_event GDK_DRAG_ENTER no_expose_event GDK_DRAG_LEAVE window_state_event GDK_DRAG_MOTION GDK_DRAG_STATUS GDK_DROP_START GDK_DROP_FINISHED GDK_CLIENT_EVENT GDK_VISIBILITY_NOTIFY GDK_NO_EXPOS E GDK_SCROLL GDK_WINDOW_STATE GDK_SETTING Per connettere una funzione ad uno di questi eventi dovremmo,scrivere : g_signal_connect (G_OBJECT (button), "button_press_event", G_CALLBACK (button_press_callback), NULL); la funzione di call_back andrà dichiarata in questo modo : static gboolean button_press_callback( GtkWidget *widget, GdkEventButton *event, gpointer data ); Le API di GDK Selezione & drag-n-drop, emettono degli eventi che sono identificati come segnali : selection_received selection_get drag_begin_event drag_end_event drag_data_delete drag_motion drag_drop drag_data_get drag_data_received Esaminando in dettaglio il programma Questa è la funzione che viene chiamata, quando si clicca sul pulsante static void hello( GtkWidget *widget, gpointer data ) { g_print ("Hello World\n"); } Questa seconda funzione è un poco più speciale in quando occorre quando il window manager invia l'evento all'applicazione. Il valore ritornato dalla funzione è conosciuto come, l'azione da intraprendere : – – TRUE : non distruggere. FALSE : invia segnale distruggere. static gboolean delete_event ( GtkWidget *widget,GdkEvent *event, gpointer data ) { g_print ("delete event occurred\n"); return TRUE; } In qusto caso la funzione di call back informa gtk di terminare il programma static void destroy( GtkWidget *widget, gpointer data ) { gtk_main_quit (); } Qui vengono dichiarati due puntatori alla stutttura GtkWidget ; GtkWidget *window; GtkWidget *button; Questa funzione inizializza la libreria (toolkit) ed esamina gli argomenti trovati a linea di comando, rimuovendoli dalla lista, permettendo alla tua applicazione di esaminare i rimanendi comandi. gtk_init (&argc, &argv); Crea un nuova finestra, ma non la visualizza. window = gtk_window_new (GTK_WINDOW_TOPLEVEL); Qui ci sono due esempi, per connettere il segnale all'oggetto, in questo casa la finestra. Vengono catturati due eventi “delete_event” “destroy” G_OBJECT e G_CALLBACK sono macro che eseguono codice e controlli per noi rendendo il codice più leggibile. g_signal_connect (G_OBJECT (window), "delete_event", G_CALLBACK (delete_event), NULL); g_signal_connect (G_OBJECT (window), "destroy", G_CALLBACK (destroy), NULL); Questa funzione è utilizzata per settare gli attributi di un oggetto contenitore. gtk_container_set_border_width (GTK_CONTAINER (window), 10); Qui vi presento una lista di altre funzioni che settano gli attributi : void gtk_widget_activate ( GtkWidget *widget ); void gtk_widget_set_name ( GtkWidget *widget, gchar*name ); gchar *gtk_widget_get_name ( GtkWidget *widget ); void gtk_widget_set_sensitive ( GtkWidget *widget,gboolean sensitive ); void gtk_widget_set_style ( GtkWidget *widget,GtkStyle GtkStyle *gtk_widget_get_style *style ); ( GtkWidget *widget ); GtkStyle *gtk_widget_get_default_style( void ); void gtk_widget_set_size_request height ); ( GtkWidget *widget,gint width,gint void gtk_widget_grab_focus ( GtkWidget *widget ); void gtk_widget_show ( GtkWidget *widget ); void gtk_widget_hide ( GtkWidget *widget ) Crea un nuovo oggetto “button” e lo connette all'evento “clicked” : button = gtk_button_new_with_label ("Hello World"); g_signal_connect (G_OBJECT (button), "clicked", G_CALLBACK (hello), NULL); Questa seconda funzione controll l'uscita dal programma che può avvenire dalla finestra “destroy” oppure costruirla noi come in questo caso da un click di un bottone. Quando l'oggetto “button” è premuto, dapprima chiama la funzione hello, successivamente chiama la nostra seconda funzoine di callback. Potresti settare tante funzioni di callback quante ne vuoi e vengono chiamate tutte in ordine. g_signal_connect_swapped (G_OBJECT (button), "clicked", G_CALLBACK (gtk_widget_destroy), G_OBJECT (window)); Addiziona al contenitore finestra l'oggetto bottone. gtk_container_add (GTK_CONTAINER (window), button); Ora dato che ogni cosa è a posto passiamo alla visualizzazione. gtk_widget_show (button); gtk_widget_show (window); Questa come dicevo è la funzione principale che gestisce tutto il GTK, e aspetta gli eventi dal server X gtk_main (); E per finire “return” che viene eseguito dopo che gtk_quit() è stata chiamata. return 0; CAPITOLO 20 Object Oriented Programing Object Oriented Programing OOP acronimo di programmazione orientata agli oggetti, in questo capitolo fornirò una visione di insieme per quanto riguarda i costrutti principali della oop, senza di volta in volta addentrarmi nei particolari che esula lo scopo del volume 1. I listati verranno commentati solo per quanto riguarda la parte di interesse, senza occupare spazio e risorse. Per tutti i listati occorre munirsi del relativo codice in assembly, ricavabile con l'ozione -S e -O0 (g++) ottimizzazione che più si avvicina a come il listato viene scritto, per il resto tutto è solo puro divertimento ! Listato 1 : #include <iostream> //************************************** using namespace std ; int stack::pop() #define SIZE 100 { class stack if (tos==0) { { int stck[SIZE] ; cout << "\nStack vuoto.\n" ; int tos ; return 1 ; public: } void init() ; --tos ; void push( int i ) ; return stck[tos] ; int pop() ; } }; //************************************* //************************************** int main ( void ) void stack::init() { { stack s1 ; tos = 0 ; stack s2 ; } //************************************** s1.init() ; void stack::push ( int i ) s2.init() ; { s1.push(1) ; if (tos==SIZE) s2.push(2) ; { s1.push(3) ; cout << "\nSTack esaurito!\n" ; s2.push(4) ; } cout << s1.pop() ; stck[tos] = i ; cout << s1.pop() ; tos++ ; cout << s2.pop() ; } cout << s2.pop() ; return 0 ; } Commento al programma : .globl main .type main: .LFB1518: pushl .LCFI8: movl .LCFI9: subl .LCFI10: main, @function %ebp %esp, %ebp $856, %esp # stack s1,s2 ; Il programma inizia in modo usuale, con la consueta funzione 'main' un salvataggio di parametri e l'allocazione in memoria dei due oggetti. L'oggetto stack ha una dimensione 'size' di 404 byte : int stck[SIZE] ; int tos ; 100 * int = 400 byte 1 * int = 4 Il 'cpp' tratta gli oggetti da un punto di vista delle variabili, il codice in se stesso non viene duplicato, altrimenti avremo montagne di byte ripetuti e porterebbe a poca efficienza. I metodi degli oggetti, vengono identificati da funzioni e a loro come parametro viene passato solo l'indirizzo dell'oggetto in questione, poi tramite un 'offset' verrà recuperata l'esatta posizione del dato La parte di inizializzazione viene gestita con i costuttori approppriati, anche se in questo semplice programma non possiamo ancora parlare di costruttori, come vedete dalla prossima tabella %eax viene caricato con l'indirizzo di partenza dell'oggetto. s1.init() ; s2.init() ; leal movl -424(%ebp), %eax %eax, (%esp) call _ZN5stack4initEv leal -840(%ebp), %eax movl %eax, (%esp) call _ZN5stack4initEv Per entrambi gli oggetti viene solo passato alla medesima funzione, solo l'indirizzo dell'oggetto interessato. .globl _ZN5stack4initEv .type _ZN5stack4initEv, @function # il metodo Init è trattata al pari di una funzione _ZN5stack4initEv: .LFB1512: pushl %ebp .LCFI0: # come tutte le funzioni il punto di ingresso è la rispettiva label: # come convenzione di chiamata viene salvato lo stack pointer movl %esp, %ebp movl 8(%ebp), %eax movl $0, 400(%eax) # %eax viene dell'oggetto popl %ebp # &(obj).tos = 400(%eax) viene impostato a zero .LCFI1: ret .LFE1512: .size _ZN5stack4initEv,.-_ZN5stack4initEv caricato con l'indirzzo # come da programma # la restante parte è rimane al # compilatore per calcolare il 'size' Metodo push : s1.push(1) ; movl $1, 4(%esp) leal -424(%ebp), %eax movl %eax, (%esp) call _ZN5stack4pushEi I commenti appaiono superflui, viene spinto come primo parametro nello stack il primo valore che l'oggetto d1 dovrà ricevere, e quindi come 'consuetudine' verrà caricato l'indirzzo dell'oggetto in %eax. La gestione della funzione non è dissimile dalle altre, per cui ne commenterò solo una. N.B. Come avete certamente notato, dall'inizio del libro, fino a questo capitolo e per i restanti, i commentti e le spiegazioni sono andate via via sempre più diradandosi, questo per il fatto, che crescendo la padronanza con : il linguaggio assembly; il commento per voi 'deve' iniziare ad essere superfluo; quando parlo il linguaggio colloquiale con qualcuno, non mi faccio rispiegare le cose 'commentandole' lo capisco e basta. Così la programmazione deve diventare un secondo linguaggio, capire cosa sta facendo il programma, così come è scritto. Questo servirà moltissimo a 'debugare' il codice di un eseguibile alla ricerca di 'bug', meglio per sviscerare e carpirne il funzionamento. Altrimenti la programmazione in assembly non ha senso meglio imparare il 'Basic' la parte visuale e programmare con il drag 'n drop e il copia ed incolla. Ma questi non sono 'veri programmatori'. Funzione membro stack.push : .globl _ZN5stack4pushEi .type _ZN5stack4pushEi, @function _ZN5stack4pushEi: .LFB1514: pushl %ebp .LCFI2: movl %esp, %ebp subl $8, %esp movl 8(%ebp), %eax cmpl $100, 400(%eax) jne .L3 movl $.LC0, 4(%esp) movl $_ZSt4cout, (%esp) .LCFI3: .LCFI4: # 8(%ebp) = &obj # 12(%ebp) = parametro 2 ex. (push 1) if (tos==SIZE) { cout << "\nSTack esaurito!\n" ; } call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT %ecx = &obj _ES5_PKc %eax = &obj .L3: &edx = *(obj).tos movl 8(%ebp) , %ecx %eax = primo parametro push movl 8(%ebp) , %eax movl 400(%eax) , %edx movl 12(%ebp) , %eax movl %eax, (%ecx,%edx,4) movl 8(%ebp), %eax incl 400(%eax) # ++ (*obj).tos leave ret movl %eax, (%ecx,%edx,4) # ( &obj, *(obj).tos * 4 ) cntinua con l'incremente dello stack ed il ritorno al normale lavoro Funzione membro stack.pop : .globl _ZN5stack3popEv .type _ZN5stack3popEv, @function # metodo push _ZN5stack3popEv: .LFB1516: # salva lo stack pointer pushl %ebp .LCFI5: # 8(%ebp) = &obj movl %esp, %ebp subl $24, %esp .LCFI6: # mette in %eax l'indirizzo dell'oggetto .LCFI7: movl 8(%ebp), %eax # confronta se &(obj).tos è 0 cmpl $0, 400(%eax) #{ jne .L5 cout << "\nStack vuoto.\n" ; movl $.LC1, 4(%esp) return 1 ; movl $_ZSt4cout, (%esp) #} call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT _ES5_PKc movl $1, -4(%ebp) jmp .L4 # %eax = &obj .L5: # *(obj).tos -- movl 8(%ebp) , %eax decl 400(%eax) movl 8(%ebp) , %edx # %eax = &obj movl 8(%ebp) , %eax # %eax = *(obj).tos movl 400(%eax) movl (%edx,%eax,4), %eax movl %eax, -4(%ebp) movl -4(%ebp), %eax # %edx = &obj , %eax .L4: leave ret movl (%edx,%eax,4), %eax movl -4(%ebp), %eax # movl &obj, *obj.tos * 4 , %eax # valore di ritorno In %eax viene messo il valore di ritorno come richiesto da stack.pop() La restante parte del programma, è solo noiosa viene chiamato lo 'stream' stdout per visuallizzare il carattere come da funzione 'cout' movl %eax, 4(%esp) movl $_ZSt4cout, (%esp) call _ZNSolsEi Come potete vedere, nell'esempio sottostante vi ho riportato i nomi delle funzioni utulizzate, dal compilatore per dare un nome agli oggetti. I nomi variano da un compilatore ad un altro. .ident "GCC: (GNU) 3.3.5 (Debian 1:3.3.5-13)" : call _ZN5stack4initEv call _ZN5stack4pushEi call _ZN5stack4pushEi call _ZNSolsEi Più avanti per quanto riguarda l'overload di funzione vedremo che i nomi portno con se qualche informazioni aggiuntiva. OVERLOADING Come accennato precedentemente vediamo la gestione da parte del 'cpp' dell'overloading di funzione : #include <iostream> double aabs( double d ) { using namespace std ; return d<0.0 ? -d : d ; } int aabs( int i); double aabs( double d ) ; int main ( void ) { int aabs( int i ) cout << aabs ( -1 ) << "\n" ; { cout << aabs ( -2.1 ) ; return i<0 ? -i : i ; return 0 ; } } ... movl $-1, (%esp) # carica lo stack con un INTERO call _Z4aabsi # chiama la funzione approppriata movl %eax, 4(%esp) movl $_ZSt4cout, (%esp) call _ZNSolsEi # visualizza il risultato # cout << aabs ( -1 ) << "\n" ; movl $.LC2, 4(%esp) movl %eax, (%esp) call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc ... # .string "\n" # push $.LC2 # anche in questo caso la gestione non è dissimile dalla precedente .LC3: .long -858993459 .long -1073689396 ... fldl .LC3 # st(0) = -2.1 fstpl (%esp) # carica lo stack con l'indirizzo call _Z4aabsd # gestisce correttamente il valore double fstpl 4(%esp) movl $_ZSt4cout, (%esp) call _ZNSolsEd # visualizza un double ... Il commento delle due funzioni a questo punto risulta superfluo, in quanto come avete potuto vedere il cmpilatore ha generato la chiamata corretta alle due funzioni, con nomi uguali ma con parametri diversi : call _Z4aabsi call _Z4aabsd Nella sua generalità la funzione può essere cos'ì scomposta : _Z4 (nome funzione) (tipo parametri) Lo potete anche vedere dalla gestione del 'COUT' : call _ZNSolsEi # cout << INTERI call _ZNSolsEd # cout << DOUBLE N.B. Per quanti linguaggi di programmazione possono esserci, ne esiste uno solo e questo è l'assembly, meglio il 'codice macchina' che è l'unico interpretabile dalla macchina, tutto il resto è una facilitazione 'menomale' alla programmazione a basso livello. Come avete notato, per quanto i linguaggi ed il sorgente possa sembrare diverso, tutto è riconducile all'assembly. (per fortuna comunque ci sono i linguaggi di alto livello, altrimenti per progettare un'interfaccia grafica, non basterebbe un viaggio su marte) Costruttori e Distruttori Ora possiamo introdurre a differenza del listano numero 1, il concetto di costruttore e distruttore tipico dei linguaggi orientati agli oggetti : #include <iostream> using namespace std ; //************************************** #define SIZE 100 int stack::pop() { class stack if (tos==0) { { int stck[SIZE] ; cout << "\nStack vuoto.\n" ; int tos ; return 1 ; public: } stack() ; --tos ; ~stack() ; return stck[tos] ; void push( int i ) ; } int pop() ; //************************************* }; int main ( void ) //************************************** { stack::stack() stack s1 ; { stack s2 ; tos = 0 ; } s1.push(1) ; //************************************** s2.push(2) ; stack::~stack() { tos = 0 ; } s2.push(4) ; cout << s1.pop() ; void stack::push( int i ) { if (tos==SIZE) { cout << "\nSTack esaurito!\n" ; } stck[tos] = i ; tos++ ; } s1.push(3) ; cout << s1.pop() ; cout << s2.pop() ; cout << s2.pop() ; return 0 ; } Il listato è simile per il funzionamento al primo listto presentato all'inizio del capitolo, tuttavia l'inizializzazione dei vari componenti ora è implicata, al momento in cui l'oggetto viene costruito (costruttore). Ancora possiamo far si che terminato lo 'scope' dell'oggetto questo possa essere disttrutto o quant'altro (distruttori). stack s1 ; stack s2 ; leal -424(%ebp), %eax leal -840(%ebp), %eax movl %eax, (%esp) movl %eax, (%esp) call _ZN5stackC1Ev call _ZN5stackC1Ev Ora al momento dell'allocazione dell'oggetto, il costruttore viene chiamato implicitamente. ~s1.stack ~s2stack leal -424(%ebp), %eax movl %eax, (%esp) call _ZN5stackD1Ev leal movl call -840(%ebp), %eax %eax, (%esp) _ZN5stackD1Ev Costruttore .globl _ZN5stackC1Ev .type _ZN5stackC1Ev, @function _ZN5stackC1Ev: .LFB1515: pushl %ebp .LCFI2: movl %esp, %ebp .LCFI3: movl 8(%ebp), %eax movl $0, 400(%eax) popl %ebp ret Distruttore .globl _ZN5stackD1Ev .type _ZN5stackD1Ev, @function _ZN5stackD1Ev: .LFB1520: pushl %ebp .LCFI6: movl %esp, %ebp .LCFI7: movl movl popl 8(%ebp), %eax $0, 400(%eax) %ebp ret Volutamente le funzioni le ho lasciate simili; l'unico scopo è quello di riazzerare la variabile membro (tos). I costruttori vengono chiamati implicitamente senza doversene preoccupare, anche per quanto riguarda l'ereditarietà, vengono chiamati in fila, dall'oggetto chiamate sino ad arrivare all'oggetto di base. Ereditarieta' Iniziamo ora ad esaminare come il 'cpp' gestisce l'ereditarieta : #include <iostream> class line : public point using namespace std ; { int l ; // public: ereditarieta line(int _x, int _y, int _l ) : class point point ( _x,_y ) { { int x ; l = _l ; int y ; } public: ~line() point( int _x, int _y ) { { l=0; x = _x ; } y = _y ; }; } //************************************* ~point() int main ( void ) { { x=0; line a( 1,1,10 ) ; y=0; } } return 0 ; } L'oggetto misura '12 byte' , questo per quanto riguarda le variabili (x,y,l) intere. la funzione main (vedi i commenti) inizializza e distrugge al tempo stesso l'oggetto tramite la chiamata al costruttore dell'oggetto 'line'. Come convenzione il 'g++' dopo il nome della funzione indica il numero ed il tipo di parametri : call _ZN4lineC1Eiii # a(1,1,10) movl $10, 12(%esp) # parametro 3 movl $1, 8(%esp) # parametro 2 movl $1, 4(%esp) # parametro 1 leal -24(%ebp), %eax # carica l'indirizzo di : a movl %eax, (%esp) call _ZN4lineC1Eiii leal -24(%ebp), %eax movl %eax, (%esp) call _ZN4lineD1Ev movl $0, %eax .L2: leave ret # a(1,1,10) # ~a Di seguito la chiamata al costuttore line : .type _ZN4lineC1Eiii, @function _ZN4lineC1Eiii: .LFB1531: pushl %ebp .LCFI6: movl %esp, %ebp .LCFI7: subl $24, %esp .LCFI8: movl 16(%ebp) movl %eax movl 12(%ebp) , %eax , 8(%esp) , %eax movl %eax , 4(%esp) movl 8(%ebp) , %eax movl %eax call , %edx movl 20(%ebp) , %eax movl %eax leave ret # 1 parametro 1 #8 indirizzo dell'oggetto , (%esp) _ZN5pointC2Eii movl 8(%ebp) # 1 parametro 2 , 8(%edx) # chiama il costuttore Point e passa (1,1) chiamata implicita al costuttore 'point' .type _ZN5pointC2Eii, @function _ZN5pointC2Eii: .LFB1534: # 0 (%edx) == &(obj).x pushl %ebp movl %esp, %ebp movl 8(%ebp) , %edx movl 12(%ebp) , %eax # %edx = &obj movl %eax , (%edx) # %eax = 1° parametro movl 8(%ebp) , %edx movl 16(%ebp) , %eax movl %eax popl %ebp # 4(%edx) == &)obj).y .LCFI15: .LCFI16: , 4(%edx) # %eax = 2° parametro ret Come avviente per i costruttori, anche i distruttori seguono lo stesso ordine di chiamata : .type _ZN4lineD1Ev, @function _ZN4lineD1Ev: .LFB1532: # schema di come sono allocati pushl %ebp movl %esp, %ebp .LCFI9: .LCFI10: subl $8, %esp # in memoria le variabili 0 ( %eax) == a.x 4(%eax) == a.y 8(%eax) == a.l .LCFI11: movl 8(%ebp), %eax movl $0, 8(%eax) #l=0; movl 8(%ebp), %eax %eax = &obj movl %eax, (%esp) call _ZN5pointD2Ev .L11: leave ret # chiama in ordine il costuttore base Funzioni FRIEND Le funzioni friend o anche classi friend hanno lo scopo di accedere ai membri privati di una classe, col permesso ovviamente della stessa. #include <iostream> int getX ( point x) { using namespace std ; return x.x ; } // friend int getY ( point x) { class point { return x.y ; } int x ; int y ; ; public: //************************************* int main ( void ) friend int getX(point x) ; { friend int getY(point y) ; point a( 1,1 ) ; point( int _x, int _y ) cout << getX( a ) ; { x = _x ; y = _y ; } ~point() cout << getY( a ) ; return 0 ; } { x=0; y=0; } }; Tralascerò tutta la parte di inizializzazione per concentrarmi sulle funzioni friend; Del resto ho già discusso ampiamente in precedenza. Per quanto riguarda l'inizializzazione non cambia nulla Questa è la routine di gestione della funzione friend # carica %eax con indirizzo oggetto leal -40(%ebp) , %eax movl %eax , (%esp) call _Z4getX5point movl %eax # invocala funzione 'amica' , 4(%esp) # salva il valore di ritorno per cout La fuzione friend, gestisce l'oggetto così com'è, dall'indirizzo di partenza viene ricavato la variabile X oppure Y a seconda del tipo di funzione e quindi ritornata. .globl _Z4getX5point .type _Z4getX5point, @function _Z4getX5point: .LFB1518: pushl %ebp .LCFI0: movl %esp, %ebp movl 8(%ebp), %eax # 0(&a).x movl (%eax), %eax # 4(&a).y popl %ebp .LCFI1: ret Funzioni INLINE Questo tipo di dichiarazioni delle funzioni è molto importante in 'cpp' in quanto consente di ottimizzare il codice in quanto le funzioni vengono espanse in linea senza dover venire richiamate con una 'call', questo va bene per piccole parte di codice ripetute, come le macro se il codice è molto , l'oggetto finale diventa pesante. #include <iostream> using namespace std ; inline int max( int a, int b ) ... { movl return a>b ? a : b ; $4, %edx ... }; int main ( void ) movl %edx { movl $_ZSt4cout, (%esp) cout << max( 3,4 ) ; call , 4(%esp) _ZNSolsEl return 0 ; } In Questo caso l'opzione -O0 mostra la funzione che viene chiamata con call, le latre ottimizzazioni, calcolano direttamente il risultato e lo visualizzano. Variabili 'Static' Una sola copia per ogni oggetto della variabile static è mantenuta e quindi condivisa per tutti gli oggetti. #include <iostream> ~point() { x=0; using namespace std ; y=0; --n ; // static } }; class point int point::n; { int x ; int main ( void ) int y ; { public: point a( 1,1 ) ; static int n ; point b( 1,1 ) ; point( int _x, int _y ) point c( 1,1 ) ; { ++n ; cout << c.n ; x = _x ; return 0 ; y = _y ; } } Di seguito mostro l'inizializzazine della variabile statica comune a tutti gli oggetti : .globl _ZN5point1nE .bss # alloca lo spazio in run-time .align 4 # allinea a intero .type _ZN5point1nE, @object # la variabile è di tipo oggetto .size _ZN5point1nE, 4 # size = 4 _ZN5point1nE: .zero # variabile 4 # riempi 4 byte con zero. '0' ... movl _ZN5point1nE, %eax # prendi la variabile statica addl $3, %eax movl %eax, _ZN5point1nE subl $3, _ZN5point1nE # destructor movl $0, %eax # return 0 movl %ebp, %esp popl %ebp # 'sempre' inizializzata a ZERO # aggiungi 3 oggetti e salva ... ret Per curiosità metto come il 'g++' ha gestito gli oggetti' movl $1, -24(%ebp) movl $1, -20(%ebp) movl $1, -40(%ebp) movl $1, -36(%ebp) movl _ZN5point1nE, %eax addl $3, %eax movl %eax, _ZN5point1nE movl $1, -56(%ebp) movl $1, -52(%ebp) # point a(1,1) # point b(1,1) # variabile static # point c(1,1) Ne più ne meno è una gestione di variabili che fa ora il compilatore, praticamente così come è impostato il programma gli oggetti sparisco e tutto si riduce a gestire 6 differenti piu una locazioni di memoria. Array di Classi #include <iostream> int point::n; using namespace std ; //************************************* // int main ( void ) array di classi { class point point a[3] = { { int x ; point(1,2), int y ; point(3,4), public: point(5,6) static int n ; } ; point( int _x, int _y ) { ++n ; cout << a[0].n ; return 0 ; x = _x ; } y = _y ; } }; Si tratta grosso modo dell'esempio precedente, tranne per il fatto che ho eliminato la gestione del costruttore che ai fini di questo capitolo non mi serviva e aggiunto la gestione degli array. Vediamo ora come GCC opera il tutto : Gestione della variabile statica : ... movl _ZN5point1nE, %eax # cout << a[0].n ; movl %eax, 4(%esp) movl $_ZSt4cout, (%esp) call _ZNSolsEi movl $0, %eax # return 0 leave ret movl $2, -52(%ebp) 52(%ebp) = -1 # usata come indice movl -48(%ebp), %eax movl -48(%ebp), %eax movl -48(%ebp), %eax movl $2, 8(%esp) movl $4, 8(%esp) movl $6, 8(%esp) movl $1, 4(%esp) movl $3, 4(%esp) movl $5, 4(%esp) movl %eax, (%esp) movl %eax, (%esp) movl %eax, (%esp) call _ZN5pointC1Eii call _ZN5pointC1Eii call _ZN5pointC1Eii leal -48(%ebp), %eax leal -48(%ebp), %eax leal -48(%ebp), %eax addl $8, (%eax) addl $8, (%eax) addl $8, (%eax) leal -52(%ebp), %eax leal -52(%ebp), %eax leal -52(%ebp), %eax decl (%eax) decl (%eax) decl (%eax) point(1,2) point(3,4) point(5,6) Puntatori ad Oggetti Vediamo come vengono gestiti i puntatori agli oggetti : #include <iostream> //************************************* using namespace std ; int main ( void ) class point { { point a(1,1) ; int x ; point *p ; int y ; p=&a ; public: cout << p->n ; static int n ; point( int _x, int _y ) return 0 ; { } ++n ; x = _x ; y = _y ; } } Anche qui non accade nulla di spettrale tutto è nel codice : movl $1, 8(%esp) # parametro 1 movl $1, 4(%esp) # parametro 2 leal -24(%ebp), %eax # indirizzo dell'oggetto movl %eax, (%esp) call _ZN5pointC1Eii # costruisci leal -24(%ebp), %eax # carica %eax con l'indirizzo del' oggetto movl %eax, -28(%ebp) # salva l'indirizzo nella locaz. del puntatore movl %eax, 4(%esp) # passa a cout il contenuto del pointer movl $_ZSt4cout, (%esp) # cioè l'indirizzo di a ; Questo parte non riveste alcun altro interesse. Puntatori a membri di una classe #include <iostream> int main ( void ) using namespace std ; { int point::*punto ; class point int (point::*funz)() ; { point a(1) ; public: point b(2) ; int p ; int doppio() punto = &point::p ; { funz = &point::doppio ; return p+p ; } point( int _p ) cout << a.*punto ; { cout << "\n" ; cout << (b.*funz)() ; p = _p ; } return 0 ; } }; int point::*punto ; int (point::*funz)() ; // è un puntatore ad un intero dell'oggetto punto // è un puntatore ad una funzione di punto point a(1) ; point b(2) ; movl $1, 4(%esp) movl $2, 4(%esp) leal -40(%ebp), %eax leal -56(%ebp), %eax movl %eax, (%esp) movl %eax, (%esp) call _ZN5pointC1Ei call _ZN5pointC1Ei Evidenziato in grassetto nel riquadro sopra, è l'indirizzo di partenza dei due oggetti. movl $0 , -12(%ebp) movl $_ZN5point6doppioEv, -24(%ebp) // punto = &point::p ; // &point::doppio ; Esattamente in quelle locazioni vi è memorizzato la variabile e l'indirizzo. Questa parte è un poco più difficile in quanto, gestendo dei puntatori dobbiamo di volta in volta tener conto degli offset, quindi : a) leal -40(%ebp), %eax # indirizzo di base dell'oggetto addl -12(%ebp), %eax # offset movl (%eax), %eax movl %eax, 4(%esp) # cout movl $_ZSt4cout, (%esp) leal -56(%ebp) , %eax movl %eax , %edx addl -20(%ebp) , %edx movl -24(%ebp) , %eax # &point::doppio addl (%edx) , %eax # indirizzo di a.doppio decl %eax movl (%eax) , %eax movl %eax , -64(%ebp) # funz = &point::doppio leal -56(%ebp) , %eax # &b addl -20(%ebp) , %eax # &b+0 movl %eax , (%esp) # push parametro call *-64(%ebp) movl %eax, 4(%esp) movl $_ZSt4cout, (%esp) call _ZNSolsEi # *a.p b) # &b # %edx = &b # 0 .L3: # call indiretta &point::doppio Il secondo tipo di calcolo è molto più complesso, in quanto fa riferimento all'indirzzo della funzione indirettamente, “LATE BINDING”. n.b. Personalmente utilizzo questo esempio non tanto da un punto di vista della programmazione, se posso cerco di evitare le cose complicate, piuttosto mi serve quando debuggo i programmi per capire cosa stanno facendo in quel momento. Indirizzi e riferimenti #include <iostream> //************************************* int main ( void ) using namespace std ; { int a=99 ; int nega ( int *i ) { return (*i = -*i) ; cout << nega (&a ) ; } cout << nega ( a ) ; return 0 ; int nega ( int &i ) { } return (i = -i) ; } Entrambe le funzioni sono identiche, dal punto di vista della gestione degli indirizzi il compilatore genera codice identico. movl $99, -4(%ebp) cout << nega (&a ) ; cout << nega ( a ) ; POINTER REFERENCE leal -4(%ebp), %eax leal -4(%ebp), %eax movl %eax, (%esp) movl %eax, (%esp) call _Z4negaPi call _Z4negaRi movl %eax, 4(%esp) movl %eax, 4(%esp) movl $_ZSt4cout, (%esp) movl $_ZSt4cout, (%esp) call _ZNSolsEi call _ZNSolsEi Non cambia nulla neanche nella chiamata, la differenza sta che il puntatore può assumere un significato aritmetico, mentre la referenza punta sempre a quell'oggetto. Allocazione dinamica Nel cpp l'uso degli operatori di allocazione dinamica, quali 'new' e 'delete' è molto sfruttato vediamo come funziona : #include <iostream> using namespace std ; cout << *a ; int main ( void ) delete a ; { int *a ; return 0 ; } a = new int(9) ; movl $4, (%esp) # numero di byte da allocare call _Znwj # new movl $9, (%eax) # muovi 9 all'indirizzo di ritorno movl %eax, -4(%ebp) # variabile locale puntatore movl -4(%ebp), %eax # carica %eax con l'indirizzo movl %eax, (%esp) # mettilo nello stack call _ZdlPv # delete Sono molto utile queste due funzioni e al tempo stesso facili da utilizzare. Try Throw Catch In questo esempio vediamo come il 'cpp' gestisce le varie condizioni di errore che possono intercorrene nel 'run time'. #include <iostream> catch ( int e ) using namespace std ; { cout << "\n catch : " << e << " ...\n" ; int main ( void ) } { catch ( ... ) // ..................................default try { { cout << "\n try ..." ; } throw ( 99 ) ; return 0 ; cout << "\n throw 100 ..." ; } } movl $4, (%esp) call __cxa_allocate_exception movl $99, (%eax) movl $0, 8(%esp) movl $_ZTIi, 4(%esp) movl %eax, (%esp) call __cxa_throw movl -12(%ebp), %eax movl %eax, (%esp) call __cxa_begin_catch # try .L2: # throw (integer) # catch ( int e ) ... movl %eax, -12(%ebp) movl -12(%ebp), %ebx call __cxa_end_catch .L6: # end catch L'ultimo catch quello di default che vale per tuttre ha lo stesso costrutto di quello rpecedente. Funzioni Virtuali Le funzioni virtuali come dice il nome, vengono rimpiazzate da quelle della classe derivata se sono state definite, così la gestione del 'cpp' si complica in quanto bisogna tener conto di alcune tabelle virtuali, ove andrà scritto l'effettivo indirizzo della funzione. Queste tabelle sono chiamate Vtable o tabelle virtuali e sono sempre allocate all'indirizzo '0' dell'oggetto. #include <iostream> using namespace std ; int main ( void ) class base { { public: derived a ; int i ; a.set(9) ; virtual int get() { return i ; } cout << a.get() ; virtual void set(int _i) { i = _i ; } } ; return 0 ; class derived : public base { } public: int get() { return i*2 ; } void set(int _i) { i = _i*2 ; } }; Procediamo passo passo altrimenti viene fuori un pasticcio! .globl main .type main, @function main: ... leal -24(%ebp), %eax # carica l'indirizzo in %eax di a movl %eax, (%esp) call _ZN7derivedC1Ev # chiama il costruttore Derived movl $9, 4(%esp) # si prepara per a.set(9) leal -24(%ebp), %eax movl %eax, (%esp) call _ZN7derived3setEi leal -24(%ebp), %eax movl %eax, (%esp) # funzione a.get() call _ZN7derived3getEv movl %eax, 4(%esp) movl $_ZSt4cout, (%esp) call _ZNSolsEi movl $0, %eax # cout << %eax # return 0 leave ret Apparentemente, il codice main è andato lisco snza intoppi, ma andiamo a vedere bene cosa succede quando chiamamo il costuttore 'derived'. .type _ZN7derivedC1Ev, @function # costuttore derived _ZN7derivedC1Ev: .LFB1530: pushl %ebp .LCFI6: movl %esp, %ebp subl $8, %esp movl 8(%ebp), %eax movl %eax, (%esp) call _ZN4baseC2Ev movl 8(%ebp), %eax movl $_ZTV7derived+8, (%eax) .LCFI7: .LCFI8: # carica con l'indirizzo dell'oggetto # chiama il costuttore 'base' # alloca la funzione nella VTable leave ret Come potete, vedere la VTable inizia in posizione 0 dell'oggetto. Questa è la vtable di 'derived', in effetti con 'movl $_ZTV7derived+8', va ad agganciare l'esatta corrispondenza nella Vtable. _ZTV7derived: .long 0 #0 .long _ZTI7derived #4 .long _ZN7derived3getEv #8 .long _ZN7derived3setEi # 12 anche l'oggetto 'base ha una probpia Vtable' _ZTV4base: .long 0 #0 .long _ZTI4base #4 .long _ZN4base3getEv #8 .long _ZN4base3setEi # 12 E quando il costruttore 'derived' chiama a sua volta il costruttore 'base' questi, inizialmente alloca l'oggetto della sua Vtable, poi il tipo derivato lo corregge allocando la sua .type _ZN4baseC2Ev, @function _ZN4baseC2Ev: .LFB1534: pushl %ebp .LCFI16: movl %esp, %ebp .LCFI17: movl 8(%ebp), %eax movl $_ZTV4base+8, (%eax) popl %ebp ret # alloca la propria funzione virtuale provate questo opzione del compilatore, e edita il file con estensione .class : g++ -fdump-class- hierarchy program.cpp Vtable for base base::_ZTV4base: 4 entries 0 0 4 &_ZTI4base 8 base::get 12 base::set Class base size=8 align=4 base (0x416b86c0) 0 vptr=((&base::_ZTV4base) + 8) Vtable for derived derived::_ZTV7derived: 4 entries 0 0 4 &_ZTI7derived 8 derived::get 12 derived::set Class derived size=8 align=4 derived (0x416b8a40) 0 vptr=((&derived::_ZTV7derived) + 8) base (0x416b8a80) 0 primary-for derived (0x416b8a40) Vengono mostrate ulteriori informzioni relativi alle VTABLE e agli oggetti. L'oggetto base nella VTABLE ha 4 indirizzo, 2 relativi alle sue funzioni, la classe base ha una dimensione di 8 byte. (2 int) ecc.. Così potete ricavare le informazioni per la classe derived Modifica al listato .s movl $_ZTV7derived+12,%eax call *(%eax) #call _ZN7derived3setEi Questo è un altro metodo per scrivere la stessa cosa, ma interfacciandosi direttamente con le tabelle virtuali. I due metodi di indirizzamento vengono definiti rispettivamenteo : - Early binding --> call _ZN7derived3setEi in quanto la funzione viene chiamata diretamente - Late binding --> call *(%eax) in quanto la funzione viene chiamata indirettamente. Template Vediamo subito come funzionano con un semplice programmino, come potete vedere dall'esempio i template vengono gestiti come overload di funzione, anche se potrebbe sembrare una parolaccia ! #include <iostream> int main ( void ) using namespace std ; { int ia = 10 , ib = 20 ; template <class x> void swappa ( x &a, x &b ) float fa = 11.1 , fb = 22.2 ; { swappa ( ia,ib ) ; x temp ; swappa ( fa,fb ) ; temp = a ; cout << ia << "\n" << ib ; a=b; return 0 ; b = temp ; } } inzializza le variabili : movl $10, -4(%ebp) # ia = 10; movl $20, -8(%ebp) # ib = 20; movl $0x4131999a, %eax movl %eax, -12(%ebp) movl $0x41b1999a, %eax movl %eax, -16(%ebp) # fa = 11.1 # fb = 22.2 swappa ( ia,ib ) ; swappa ( fa,fb ) ; leal -8(%ebp), %eax leal -16(%ebp), %eax movl %eax, 4(%esp) movl %eax, 4(%esp) leal -4(%ebp), %eax leal -12(%ebp), %eax movl %eax, (%esp) movl %eax, (%esp) call _Z6swappaIiEvRT_S1_ call _Z6swappaIfEvRT_S1_ Classi Generiche Cosi come i template permettono tipi generici anche le classi posso diventare generiche : #include <iostream> using namespace std ; template <class StackType> StackType stack<StackType>::pop() { if (tos==0) { #define SIZE 100 cout << "\nStack vuoto.\n" ; template <class StackType> class stack { StackType stck[SIZE] ; int tos ; return 1 ; } --tos ; return stck[tos] ; }; public: stack( void ) { tos=0; } void push( StackType i ) ; StackType pop() ; int main ( void ) { stack<int> si ; stack<char> sc ; }; si.push(1) ; template <class StackType> void stack<StackType>::push( StackType i ) si.push(3) ; { sc.push('a') ; if (tos==SIZE) sc.push('b') ; { cout << si.pop() ; cout << "\nSTack esaurito!\n" ; cout << sc.pop() ; } stck[tos] = i ; return 0 ; tos++ ; } } Non mi soffermerò a lungo in quanto, delinerò una spiegazione sommaria su come funziona : leal movl -424(%ebp), %eax %eax, (%esp) call _ZN5stackIiEC1Ev leal movl -536(%ebp), %eax %eax, (%esp) call _ZN5stackIcEC1Ev Nella tabella sopra esposta sono presenti i due costruttori per i due differenti oggetti indicati nel tamplate. Per quanto riguarda la gestione, vengono delineate tante 'funzioni' differenti per gestire i tipi primitivi quanto richesto dal template. movl $1, 4(%esp) movl $98, 4(%esp) leal -424(%ebp), %eax leal -536(%ebp), %eax movl %eax, (%esp) movl %eax, (%esp) call _ZN5stackIiE4pushEi call _ZN5stackIcE4pushEc Nel nome di queste 2 funzioni cambia solamente la 'i' con la 'c' a sottolineare la gestione di interi da caratteri. I template sono di sicuro un modo per semplificarsi la vita, meno certo per il compilatore! hi hi ... Al di la' di queta premessa, lo studio può benessimo essere ricondotto al capitolo uno e alla gestione base della classe 'stack'. Iteratori L'ultimo aspetto che vedremo sul cpp, #include <iostream> #include <vector> #include <cctype> # preferisco riscrivere i commenti passo passo using namespace std ; # per una miglior comprensione int main ( void ) { vector<char> v(10) ; // 24 movl $10, 4(%esp) leal -24(%ebp), %eax movl %eax, (%esp) .LEHB0: vector<char>::iterator p; // 28 call _ZNSt6vectorIcSaIcEEC1Ej leal -28(%ebp), %eax movl %eax, (%esp) LEHE0: .LEHB1: call _ZN9__gnu_cxx17__normal_iteratorIPcSt6vectorI cSaIcEEEC1Ev p = v.begin() ; // 28 <- 24 leal -24(%ebp), %eax movl %eax, 4(%esp) movl %edx, (%esp) call _ZNSt6vectorIcSaIcEE5beginEv subl $4, %esp movl -32(%ebp), %eax movl %eax, -28(%ebp) int i = 0 ; movl while ( p != v.end() ) { *p = 'a' + i ; cout << *p ; $0, -36(%ebp) leal -40(%ebp), %edx leal -24(%ebp), %eax movl %eax, 4(%esp) movl %edx, (%esp) call _ZNSt6vectorIcSaIcEE3endEv subl $4, %esp leal -40(%ebp), %eax movl %eax, 4(%esp) leal -28(%ebp), %eax ... ++p ; ++i ; } return 0 ; } In questo caso, gli iteratori sono delle comuni locazioni di memoria, contenenti riferimenti ad indirizzi, detto in modo molto semplicistico. Prendete, il listato solo come un veloce esempio di come lavora il g++ e non certo come un approfondimento, Più che altro l'ho inserito, per definire qualche aspetto sul lavoro del compilatore e del g++. Tutto qui. CAPITOLO 21 Glibc & SYSCALL Glibc & SYSCALL Prima di passare come consuetudine agli esempi ed alla breve spiegazione occorre introdurre alcuni concetti fondamentali del sistema operativo linux. Innanzitutto quello che definiamo linux, cioè quello che ha scritto Torvalds è solamente il kernel! il resto ci arriva da progetto GNU della Free Software Foundation; Pertanto parlando di questo sistema operativo è meglio identificarlo come GNU/Linux! GNU/Linux è composto da un kernel, tipico dei sistemi Unix, (Unix/like) identificato come il nucleo di sistema. Questa parte del s.o ovviamente è la più importante e ha come compito quella di dialogare a basso livello con l'hardware della macchina. La restante parte che gestisce le risorse viene realizzato tramite programmi che vengono gestiti dal kernel, quindi tutti questi processi accedono alle risorse hardware tramite il kernel! Ecco perchè in linux l'acceso diretto alla memoria, esempio memoria video $0xb800 come in Dos/Windows, lo si può fare se non attraverso il kernel. Il kernel è l'unico 'programma' quindi ad essere eseguito in modalità privilegiata, con il completo accesso all'hardware mentre i programmi vengono eseguiti in modalità protetta. Il sistema operativo GNU/Linux è un sistema Multitasking e MultiUtente. Più utenti connessi alla stessa macchina è consentito eseguire più programmi contemporaneamente; in realtà i programmi vengono gestiti sequenzialmente uno dopo l'altro tramite uno 'scheduler' una parte del kernel che si occupa di stabilire ad intervali fissi e secondo alcune priorità il programma che deve essere esguito in quel momento, questa tecnica viene chiamata 'preemptive scheduling '. Ancora il kernel si occupa di gestire la memoria attraverso un meccanismo di 'memoria virtuale', ad ogni processo viene assegnato uno spazio di indirizzi virtuale. Ogni processo in qeusto caso pensera' di essere l'unico processo in esecuzione sulla macchina e di aver tutta la memoria a completa disposizione. La memoria viene gestita attraverso, una serie di rimappature di tale e prelevata dalla memoria di swap quando serve. La memoria in un sistema Gnu/linux viene suddivisa in pagine. User/Kernel Space Il concetto dello User/kernel Space è fondamentale nei sistemi Unix-Like. Dicevamo che ogni processo viene eseguito in un determinato spazio in memoria, meglio ogni processo crede di aver a disposizione tutta la memoria della macchina, Ricordare il programma che allocava la memoria 'ogni locazione è una bugia!'. Questo accade in quanto al processo viene assegnato uno 'spazio' chiamato user space. Questo spazio virtuale identifica lo spazio che il kernel fornisce al programma per essere elaborato. Differentemente il kernel, viene eseguito ad un livello privilegiato gli viene assengato un kernel space, questo viene fatto per motivi di sicurezza in quanto ogni programma/proceso avendo il suo personale user space non va ad intaccare le strutture di programmi vicini e d'altro canto non può accedere alle risorse della macchina se non tramite il kernel, che gira in uno spazio inaccessibile dai comuni programmi appunto il kernel space. Per tanto ribadisco il concetto fondamentale dei sistemi Unix-like, che se un programma o a chi programma necessità di utilizzare direttamente l'hardware della macchina lo potrà fare solo attraverso il Kernel. Ancora l'hardware viene gestito dall'interno del kernel. Nello user space il programmatore utilizzerà le interfacce messe a disposizione dal kernel. GLIBC & SYSCALL Commentiamo questo schema : CPU MEMORIA DISCO KERNEL SYSTEM CALL KERNEL SPACE GNU C LIB processo 1 processo 2 processo 3 ... USER SPACE Questo è un riassunto del paragrafo precedente ed illustra visivamente la distinzione tra kernel e user space e quali programmi vi fanno parte. Come vedete tramite le 'system call' il processo può gestire l'hardware, queste tramite l'int $0x80 generano un'interruzione e passano il controllo al kernel. Le stesse system call vengono rimappate nelle opportune chiame di funzione della libreria C, che oltre alle chiamate di sistema definisce anche tutta un'altra serie di funzione standard. Quindi programmare in linux significa quindi essere in grado di utilizzare le varie interfacce della libreria C. Digitate : debian:~# man 2 exit Riformattazione di exit(2), attendere prego... debian:~# man 3 exit Riformattazione di exit(3), attendere prego... debian:~# Di seguito vi fornisco un breve output, riguardante le chiamate di sistema (syscall) e le funzioni della libreria C, potete farvi da solo un idea della rimappatura dell syscall. unistd.h --> come avevamo visto in precedenza elenco delle syscall. stdlib.h --> libreria standard del C. _EXIT(2) Linux Programmer's Manual _EXIT(2) NAME _exit, _Exit - terminate the current process SYNOPSIS #include <unistd.h> void _exit(int status); #include <stdlib.h> void _Exit(int status); EXIT(3) Linux Programmer's Manual EXIT(3) NAME exit - cause normal program termination SYNOPSIS #include <stdlib.h> void exit(int status); DESCRIPTION ... La struttura della memoria per un processo kernel kernel 0xFFFF:FFFF STACK stack 0xC000:0000 HEAP allocazione dimaminca 0x0800:xxxx .bss dati non inizializzati .data dati inizializzati .text codice KERNEL SPACE USER SPACE 0x0800:0000 In questo schema viene rappresentata la memoria virtuale in riferimento ai vari segmenti di memoria, rapportati al kernel space e user space. Avvio/termine dei processi Il processo è l'unità base di Gnu/linux, ogni processo per essere avviato deve essere avviato a sua volta da un altro processo attraverso la syscall 'exec' o famiglia. Il primo processo che il kernel avvia è 'init'. Questo è il padre di tutti i processi Questi processi a meno che non siano linkati staticamente con le proprie librerie si avviano in modo incompleto, un altro programma i linux il 'Dynamic loader linker' viene chiamato al fine di invocare le librerie richieste dal processo e linkarle in run-time. Avvio di un processo : 1) 2) 3) 4) 5) shell ; dalla shell viene invocato il processo ; exec prog ; questo viene eseguito attraverso exec ld-linux-so ;viene chiamato in causa il link loader shared-lib ; vengono caricate le eventuali libreria processo. viene caricato il processso ; Quando un processo deve terminare, viene invocata la syscall 'exit' che informa il kernel di rilasciare lo spazio occupato dal programma. (oppure tramite la funzioen abort). abbiamo già usato la funzione exit che ritornava un valore da 0a 255 alla shell. 6) exit ; il processo viene terminato ; .data # PROGRAMMA uno.s .data str_begin: strbinsh: .asciz "\nSono il programma UNO\n" .string .equ str_len , . - str_begin strfn: .string .text # PROGRAMMA due.s "/bin/sh" "uno" .text .global _start .global _start _start: _start: #.............. sys call execve movl $11 movl $4 , %eax movl $strfn movl $1 , %ebx movl $0 , %ecx movl $str_begin , %ecx movl $0 , %edx int $0x80 #.............. sys call write movl $str_len int $0x80 , %edx , , %ebx #.............. sys call exit movl $1 , #.............. sys call exit xorl %ebx,%ebx movl $1,%eax int $0x80 xorl %ebx,%ebx int $0x80 %eax %eax Nella tabella precedente sono riportati due programmi in assembly. il programma uno.s e due.s. Eseguendo il programma due.s si ottiene anche l'esecuzione del primo mediante la chiamata alla 'syscall exec'. Come del resto mostrato anche nella precedura da passo 1 a 6. Entrambi i programmi nel nostro esempio terminano 'normalmente' con l'invocazione alla syscall exit. In linux ogni processo viene identificato dal kernel con un pid (process identifier) che è una numero sequenziale che identifica univocamente ogni processo. Il primo processo a venir eseguito dal kernel è init e viene associato il numero di pid 1 successivamente gli altri. I processi possono venir eseguiti solo da altri processi. In altre parole il nuovo processo è figlio del processo che lo ha messo in esecuzione il padre. E' possibile identificare correttamente il processo padre. Per quanto riguarda l'uscita del programma, dobbiamo fare i conti con lo 'schedulatore', in quanto non è detto che il processo figlio termini prima del processo padre. In questo caso linux per un breve periodo di tempo avrà dei processi detti 'zombie' e gli verrà dato come padre il primo processo l'init. Per una trattazione più esplicita fate riferimento al vol 2 di questa pseudo collana. La system call exec accetta come parametri in ingresso il nome del file e la lista degli argomenti *argv e *env, come un array di pointer e termina la lettura della lista quando viene incontrato un puntatore a NULL, quindi se non volete passare argomenti %ecx e %edx andranno azzerati. Nei listati successivi viene illustrato il funzionamento della syscall getpid e getppid. debian:~/prova# ./uno.bin Sono il processo UNO.S con pid <4236> # questo è il processo figlio Sono il processo UNO.S con ppid <4235> Sono il processo UNO.S con pid <4235> # questo è il processo padre Sono il processo UNO.S con ppid <4193> debian:~/prova# sys call 20 getpid sys call 64 getppied sys call 2 fork ; ritorna come valore il processo in %eax ; ritorna come valore il processo del padre in %eax ; crea un processo figlio ; il prototipo di funzione in c è : #include <sys/types.h> #include <unistd.h> pid_t getpid ( void ) ; pid_t getppid ( void ) ; pid_t fork ( void ) ; pid_t è un 'unsigned short' può assumere i valori da '0 a 32768' vengono assegnati sommando uno all'ultimo processo, e ripartendo da 300 se si raggiunge la fine. N.B. Nei miei esempi mi vedete sempre loggato come root, DA NON FARE MAI !!!! crea un processo figlio : .data #...................... printf pid pushl %eax str_1: pushl $str_1 .asciz "\nSono UNO.S con pid <%d> \n" call printf str_2: addl $8,%eax .asciz "\nSono UNO.S con ppid <%d> \n" #................ sys call get ppid .text movl $64,%eax int $0x80 #...................... printf pid .globl _start pushl %eax pushl $str_2 _start: #...................sys call fork call printf movl $2,%eax addl $8,%eax int $0x80 #.................... sys call exit #................. sys call get pid movl $1,%eax movl $20,%eax xorl %ebx,%ebx int $0x80 int $0x80 Directory Esaminiamo ora alcune chiamate di sistema per manipolare le directory .data movl $183 .equ SIZE_T,256 movl $strcwd ,%ebx # anche fino a 1012 movl $SIZE_T ,%ecx .bss int .lcomm strcwd,SIZE_T pushl $strcwd .text call .global _start addl _start: ,%eax $0x80 puts $4,%esp #............... exit #............. get CWD movl $1,%eax xorl %ebx,%ebx int $0x80 La syscall 183, getcwd, ritorna il pathname, della directory corrente all'indirizzo specificato, occorre preallocare la dimensione del path name, 256 caratteri in alcune estensioni anche a 1012. GETCWD(3) NAME Linux Programmer's Manual GETCWD(3) getcwd, get_current_dir_name, getwd - Get current working directory SYNOPSIS #include <unistd.h> char *getcwd(char *buf, size_t size); char *get_current_dir_name(void); char *getwd(char *buf); Di seguito commento altre tre funzioni della chiamate di sistema con i relativi esempi : %eax %ebx - sys mkdir [39] pathname - sys chdir [12] pathname - sys rmdir [40] pathname ############################## macro # chiama la syscall CHDIR .macro SYSCHDIR DIRNAME movl $12,%eax # numero sys call movl \DIRNAME,%ebx # passa a %ebs il pathname int $0x80 .endm ############################### data # alloca spazio per pathname .equ SIZE_T,256 # anche fino a 1012 .bss .lcomm strcwd,SIZE_T ############################## # pathname nuova directory .data newdir: .asciz "NUOVADIR" backdir: .asciz ".." .text # directory di ritorno ################################ main .global _start _start: call printCWD # visualizza current working directory #.................sys call mkdir movl $39 ,%eax # [39] sys call mkdir movl $newdir,%ebx # path name directory movl $0777 # assegna i permessi alla directory ,%ecx int $0x80 # vedi tabella successiva SYSCHDIR $newdir call printCWD SYSCHDIR $backdir # invoca la macro per tornare alla dir base call printCWD #................. sys call rmdir movl $40 ,%eax movl $newdir,%ebx int $0x80 jmp exit # [40] sys call rmdir # pathname Non mi dilungo nella spiegazione di questo listato in quanto facilmente intuibile. ############################## printCWD .globl printCWD .type printCWD, @function printCWD: push %ebp movl %esp,%ebp movl $183 movl $strcwd ,%ebx int $0x80 ,%eax # syscall getcwd [183] pushl $strcwd call puts addl $4,%esp movl %ebp,%esp popl %ebp ret ############################## exit exit: movl $1 ,%eax xorl %ebx ,%ebx int $0x80 # sys call exit [1] Tabella relativa ai permessi : MODO VALORE SIGNIFICATO S_ISUID 04000 SET USER ID S_ISGID 02000 SET GROUP ID S_ISVTX 01000 STIKY BIT S_IRWXU 00700 L'UTENTE HA TUTTI I PERMESSI S_IRUSR 00400 l'UTENTE HA IL PERMESO DI LETTURA S_IWUSR 00200 L'UTENTE HA IL PERMESSO DI SCRITTURA S_IXUSR 00100 L'UTENTE HA IL PERMESSO DI ESECUZIONE S_IRWXG 00070 IL GRUPPO HA TUTTI I PERMESSI S_IRGRP 00040 IL GRUPPO HA IL PERMESO DI LETTURA S_IWGRP 00020 IL GRUPPO HA IL PERMESSO DI SCRITTURA S_IXGRP 00010 IL GRUPPO HA IL PERMESSO DI ESECUZIONE S_IRWXO 00070 IL GRUPPO HA TUTTI I PERMESSI S_IROTH 00004 GLI ALTRI A IL PERMESO DI LETTURA S_IWOTH 00002 GLI ALTRI HA IL PERMESSO DI SCRITTURA S_IXOTH 00001 GLI ALTRI HA IL PERMESSO DI ESECUZIONE FILE Avevamo già discusso in precedenza l'utilizzo delle sys call read & write. Occorre tener presente che in un sistema *nix like, tutto è un file ! “Everything is a file”, quindi anche un hard disk o cdrom /dev/hda e /dev/cdrom il sistema li vede come file ed applica read e write sui rispettivi stream di lettura e scrittura, (non a tutti i device si può applicare le stesse regole, altri device hanno anche regole proprie). Sys call OPEN [5] #.................................. open movl $SYS_OPEN movl st_argv1(%ebp) movl $RO , %eax , %ebx , %ecx movl $0666 int $SYSCALL , %edx # # # # # # sys open [5] File descriptor stringa permessi “sola lettura” vedi capitol o l'elfo ed hex RWX usr group other $0x80 #.................................. salva handler file movl %eax,(fin) Sys call READ [3] movl movl movl movl int $SYS_READ (fin) $elf_begin $elf_size $SYSCALL , , , , %eax %ebx %ecx %edx # # # # # %eax = [3] File Descriptor indirizzo blocco dimensioni del bloco $0x080 sys call WRITE [4] movl xorl movl movl int $4 %ebx $buffer $len $0x80 , , , , %eax %ebx %ecx %edx # # # # write = 4 stdin = 0 scrive a video indirizzo di partenza numero caratteri sys call CLOSE [6] movl $6 movl (fin) int $0x80 ,$eax ,%ebx # syscall close file # file descriptor Una questione di tempo #include <time.h> #include <stdio.h> #define SIZE 256 int main (void) { char buffer[SIZE]; time_t curtime; struct tm *loctime; /* Get the current time. */ curtime = time (NULL); /* Convert it to local time representation. */ loctime = localtime (&curtime); /* Print out the date and time in the standard format. */ fputs (asctime (loctime), stdout); /* Print it out in a nice format. */ strftime (buffer, SIZE, "Today is %A, %B %d.\n", loctime); fputs (buffer, stdout); strftime (buffer, SIZE, "The time is %I:%M %p.\n", loctime); fputs (buffer, stdout); } return 0; Questo listato scritto in C illustra l'utilizzo e la gestione del tempo, la codifica in assembly sarà questa. Storicamente i sistem *nix hanno sempre mantenuto al loro interno la gestione di due timpi di tempi : - calendar time : detto anche tempo di calendario è il tempo misurato dalla mezzanontte del 1 gennaio 1970 (UTC), questa data viene chiamata The Epoch Viene anche chiamato GMT (Greenwich Mean Time). Per misurare questo tempo è riservto il primitivo : time_t . - process time : detto anche tempo del processore. Viene misurato in clock tick. Numero di interruzioni effettuate dal timer di sistema. Anche questo tempo si esprime in secondi ma provvede ad una precisione maggiore rispetto al primo. .file "prova.c" .section .rodata .LC0: .string "Today is %A, %B %d.\n" .LC1: .string "The time is %I:%M %p.\n" .text .globl main .type main, @function main: pushl %ebp movl %esp, %ebp subl $296, %esp # # # # char buffer[SIZE] time_t curtime struct tm *loctime -8(%ebp) &buffer[256] -264(%ebp) &buffer[0] -268(%ebp) -272(%ebp) andl movl subl $-16, %esp $0, %eax %eax, %esp # riallinea stack movl call $0, (%esp) time # time (NULL) movl leal movl call movl %eax, -268(%ebp) -268(%ebp), %eax %eax, (%esp) localtime %eax, -272(%ebp) # localtime (&curtime) movl movl call -272(%ebp), %eax %eax, (%esp) asctime # asctime movl movl %eax, %edx stdout, %eax movl movl call %eax, 4(%esp) %edx, (%esp) fputs movl movl movl movl leal movl call -272(%ebp), %eax %eax, 12(%esp) $.LC0, 8(%esp) $256, 4(%esp) -264(%ebp), %eax %eax, (%esp) strftime loctime = local(&curtime) -264(%ebp) = &buffer[0] movl movl leal movl call stdout, %eax %eax, 4(%esp) -264(%ebp), %eax %eax, (%esp) fputs movl movl movl movl leal movl call -272(%ebp), %eax %eax, 12(%esp) $.LC1, 8(%esp) $256, 4(%esp) -264(%ebp), %eax %eax, (%esp) strftime movl movl leal movl call stdout, %eax %eax, 4(%esp) -264(%ebp), %eax %eax, (%esp) fputs movl leave ret $0, %eax # par 4 loctime # par 3 "..." # par 2 SIZE # par 1 buffer # par 2 stdout # par 1 buffer .size main, .-main .section .note.GNU-stack,"",@progbits .ident "GCC: (GNU) 3.3.6 (Debian 1:3.3.6-7)" Questa è stata una breve carrellata delle funzione del Calendar Time, questo tempo è manenuto dal kernel nalla variabile time_t (normalmente) long int. time_t time ( NULL ) : ottiene il calendar time char *asctime( struct tm *tm ) : produce una stringa partendo dal tempo attuale char *locatime( time_t current) : converte il calendar time in un formato time size_t strftime ( char *s,size_t, const char *format, const struct tm *tm ); stampa il tempo tm della stringa secondo il formato. Man strftime per vedere tutte le opzioni, ne riporto solo alcune : %a %A %b %B %c %I %p %P %m %M The abbreviated weekday name according to the current locale. The full weekday name according to the current locale. The abbreviated month name according to the current locale. The full month name according to the current locale. The preferred date and time representation for the current locale. The hour as a decimal number using a 12-hour clock (range 01 to 12). Either `AM' or `PM' according to the given time value, or the corresponding strings for the current locale. Noon is treated as `pm' and midnight as `am'. Like %p but in lowercase: `am' or `pm' or a corresponding string for the current locale. (GNU) The month as a decimal number (range 01 to 12). The minute as a decimal number (range 00 to 59). riporto la strutture tm : struct tm { int tm_sec int tm_min int tm_hour int tm_mday int tm_mon int tm_year int tm_wday int tm_isdst long tm_gmtoff const char *tm_zone } ; ; ; ; ; ; ; ; ; ; // // // // // // // // // // secondi minuti ore giorno del mese mese anno giorno della settimana ora legale secondi est di UTC time zome Questo esempio invece dimostra l'utilizzo del process time : #include <time.h> #include <stdio.h> int { main (void) int i,j ; clock_t start, end; double cpu_time_used; start = clock() ; for (i=0;i<999999;i++) ; end = clock() ; printf( "\n<%g>\n",cpu_time_used = ((double) (end - start)) ); } return 0; di seguito riporto la funzione in assembly generata con -S O3. Clock_t è equivalente ad un intero a 32 bit. .file "prova2.c" .section .rodata.str1.1,"aMS",@progbits,1 .LC0: .string "\n<%g>\n" .text .p2align 4,,15 .globl main .type main, @function main: pushl %ebp movl %esp, %ebp pushl %ebx subl $20, %esp andl $-16, %esp call movl .L6: clock %eax, %ebx # %eax = clock # salva in %ebx movl $99999998, %eax .p2align 4,,15 # for (i=0;i<99999999;i++) # { decl jns %eax .L6 # } call subl clock %ebx, %eax # %eax = clock # ottieni il tempo trascorso pushl fildl addl %eax (%esp) $4, %esp # legge un double a 64 bit movl fstpl call movl $.LC0, (%esp) 4(%esp) printf -4(%ebp), %ebx # stampa tempo di CPU # memorizza un double 64 bit e pop xorl %eax, %eax # return 0 leave ret .size main, .-main .section .note.GNU-stack,"",@progbits .ident "GCC: (GNU) 3.3.6 (Debian 1:3.3.6-7)" Questa è la stessa versione, più o meno con l'utilizzo delle syscall time : sys call times [43] #include <time.h> #include <stdio.h> int main (void) { int i; clock_t start, end; __asm__ ( "\n\t movl $43,%%eax \n\t " "\n\t int $0x80 \n\t " : "=a" (start) : ); for (i=0;i<9999999;i++) ; __asm__ ( "\n\t movl $43,%%eax \n\t " "\n\t int $0x80 \n\t " : "=a" (end) : ); printf ( "\n<%d>\n",(long)end-(long)start ); } return 0; CAPITOLO 22 Comunicazione tra Processi Comunicazione tra processi In questo capitolo, affronteremo molto velocemente alcune funzioni della libreria che permettono la comunicazione tra i vari processi. Prenderemo in considerazione l'utilizzo dei segnali, le fifo e la memoria condivisa. I Segnali Il segnale è il primo e più semplice meccanismo di comunicazione fra i processi. Il segnale porta con se solo l'informazione sul suo tipo, almeno nella sua forma base. Anche se come vedremo funzioni più complesse permettono di ricavare molte più informazioni. Quindi il segnale viene utilizzato per comunicare ad un processo, l'occorrenza di qualche evento, per esempio : – – – – una divisione per zero ; la scadenza di un timer/allarme ; una richiesta di terminazione ; un operazione illegale ... Quando un processo riceve un segnale, viene eseguita un'apposita routine (signal handler) invece del normale corso del programma. Possiamo a titolo informativo identificare due tipologia di segnali, dette semantiche : – semantica affidabile, reliable ; In questo caso la routine non resta attiva ed è compito dell'utente ripetere l'installazione del gestore del segnale, quindi se arrivano due segnali al programma un va perso. Ancora è che non esiste un modo per ignorare i segnali. – semantica inaffidabile unreliable ; In questo tipo di semantica utilizzata da Linux, il gestore una volta installato resta attivo. In questo caso abbiamo la possibilità di bloccare la consegna dei segnali. Tranne (kill). p.s. Desidero ringraziare Simone Piccardi, per aver messo a disposizione della comunità open source il suo ottimo volume per la programmazione in ambiente linux : - GAPIL – Guida Alla Programmazione in Linux Tipi di segnali In genere possiamo suddividere i segnali in tre categorie : – Errori ; In questo caso il processo ha fatto qualcosa di sbagliato e non può continuare. (divisioni x zero, istruzioni illegali ) ; – Eventi Esterni ; Ha in genere a che fare con l'I/O o con processi figli. (es. scadenza di un timer, terminazione dei processi figli); – Richieste esplicite ; Al programma è stato inviato un determinato segnale. Ancora i segnali possono essere distinti in : – sincroni : legato ad un'azione specifica del programma. – asincroni : generati da eventi fuori dal controllo stesso del programma. Notifica di un segnale Quando un segnale viene generato, il kernel prende nota del fatto nella task_struct del processo. In genere la consegna è immediata. Se il segnale è bloccato l'invio non avviene e risulta pendente. Il kernel non mantiene i segnali bloccati, li scarta automaticamente. Segnali come SIGKILL, SIGSTOP, non possono essere bloccati. Possiamo utilizzare due funzioni per manipolare i segnali : – – signal ; sigaction ; Di seguito elenco i segnali su linux, con il comando : man 7 signal, potete avere una trattazione più esplicita. man 7 signal : Signal Value Action Comment ---------------------------------------------------------------------SIGHUP 1 Term SIGINT SIGQUIT SIGILL SIGABRT SIGFPE SIGKILL SIGSEGV SIGPIPE SIGALRM SIGTERM SIGUSR1 SIGUSR2 SIGCHLD SIGCONT SIGSTOP SIGTSTP SIGTTIN SIGTTOU 2 3 4 6 8 9 11 13 14 15 30,10,16 31,12,17 20,17,18 19,18,25 17,19,23 18,20,24 21,21,26 22,22,27 Term Core Core Core Core Term Core Term Term Term Term Term Ign SIGBUS SIGPOLL SIGPROF SIGSYS SIGTRAP 10,7,10 Stop Stop Stop Stop Hangup detected on controlling terminal or death of controlling process Interrupt from keyboard Quit from keyboard Illegal Instruction Abort signal from abort(3) Floating point exception Kill signal Invalid memory reference Broken pipe: write to pipe with no readers Timer signal from alarm(2) Termination signal User-defined signal 1 User-defined signal 2 Child stopped or terminated Continue if stopped Stop process Stop typed at tty tty input for background process tty output for background process 27,27,29 12,-,12 5 Core Term Term Core Core Bus error (bad memory access) Pollable event (Sys V). Synonym of SIGIO Profiling timer expired Bad argument to routine (SVID) Trace/breakpoint trap SIGURG SIGVTALRM SIGXCPU SIGXFSZ 16,23,21 26,26,28 24,24,30 25,25,31 Ign Term Core Core Urgent condition on socket (4.2 BSD) Virtual alarm clock (4.2 BSD) CPU time limit exceeded (4.2 BSD) File size limit exceeded (4.2 BSD) SIGIOT SIGEMT SIGSTKFLT SIGIO SIGCLD SIGPWR SIGINFO SIGLOST SIGWINCH SIGUNUSED 6 7,-,7 -,16,23,29,22 -,-,18 29,30,19 29,-,-,-,28,28,20 -,31,- Core Term Term Term Ign Term IOT trap. A synonym for SIGABRT Term Ign Term Stack fault on coprocessor (unused) I/O now possible (4.2 BSD) A synonym for SIGCHLD Power failure (System V) A synonym for SIGPWR File lock lost Window resize signal (4.3 BSD, Sun) Unused signal (will be SIGS Signal Questa è l'interfaccia più semplice per la gestione dei segnali. Nel nostro esempio intercetteremo, un'istruzione illegale e ne notificheremo l'evento,'.byte 255' non può essere interpretato dalla cpu, in quando non ha senso. Questo codice viene trattato come un'istruzione illegale e ne viene quindi notificato il segnale al programma. #include <signal.h> .text typedef void sig_t(int); extern void exit(int); void catch_SIGILL(int n) { exit(12); } void ko(void) { __asm__(".long -1\n"); } int main(void) { sig_t *foo; foo = signal(SIGILL, catch_SIGILL); } .globl catch_SIGILL .type catch_SIGILL,@function catch_SIGILL: nop nop nop movl call _start: ko(); return 0; $12, (%esp) exit .globl _start pushl pushl call $catch_SIGILL $4 signal .byte 255 .byte 255 .byte 255 pushl call $0 exit root@0[prova]# gcc -ansi -pedantic -Wall sig1.c -o sig1.bin root@0[prova]# ./sig1.bin root@0[prova]# echo $? 12 root@0[prova]# Commento : Nel nostro caso possiamo vedere lo stesso programma in 'c' ed in assembly. Entrambi fanno la medesima cosa, cioè catturare il segnale dell'istruzione illegale. La funzione signal, installa un gestore per il segnale SIGILL. Quindi all'atto della notifica del segnale di istruzione illecita, verrà eseguita la routine catch_SIGILL. sig_t è un puntatore ad una funzione void, cioè senza argomento di ritorno. Ho aggiunto anche come compilare l'esempio e come potete vedere dal risultato viene visualizzato correttamente il valore '12'. SIGTERM Nel programma precedente, il segnale veniva intercettato per un errore all'interno del programma, in questo secondo caso vediamo come inviare un messaggio al programma. .section .align 32 .rodata .globl _start .strFine: .string "\n Mi hanno inviato il segnale di Term\n _start: Educatamente!\n Bye...\n" pushl $catch_SIGTERM pushl $15 .text call signal addl $8,%esp .globl catch_SIGTERM .type catch_SIGTERM, @function call getpid catch_SIGTERM: pushl %eax pushl $.LC1 movl $.strFine, (%esp) call puts call printf movl $0, (%esp) addl $8,%esp call exit call getchar .section .rodata .LC1: movl $.LC2, (%esp) .string "\nSono il processo <%d>" call printf .LC2: .string "\nTermino Regolarmente\n" movl $0, %eax xorl %ebx,%ebx int $0x80 .text Shell 1 : # ./sig2.bin Sono il processo <13650> Mi hanno inviato il segnale di Term Educatamente! Bye... # Shell 2 : # kill 13650 Commento : Viene installato il gestore per il segnale di 'SIGTERM ' che ha il numero 15, ed istruito ad eseguire la funzione catch_SIGTERM . Viene catturato il pid del programma, cosicchè dato questo valore è possibile terminarlo da una seconda shell. Come potete vedere il programma resta in attesa della pressione di un tasto, oppure dell'istruzione : kill <pid>. Allarmi In questo modo è possibile eseguire una funzione handler dopo un determinato periodo di tempo : .section .rodata .LC0: .string "Allarme!" .text .globl catch_SIGALARM .type catch_SIGALARM, @function catch_SIGALARM: movl call $.LC0, (%esp) puts movl call $0, (%esp) exit .section .rodata .LC1: string "\nHo puntato il timer 5 sec\n" .globl _start _start: movl movl call $catch_SIGALARM,4(%esp) $14, (%esp) # alarm signal pushl pushl call addl %eax $5 alarm $8,%esp movl call $.LC1, (%esp) printf call getchar movl call $0, %eax exit # 5 secondi .text # ./sig3.bin Ho puntanto il timer 5 sec Allarme! # Commento : Come nei due esempi precedenti, viene settato un tipo segnale (allarme/14) attraverso la funzione signal. Dopo un attesa di 5 secondi viene richiamata la funzione handler. SIGACTION La gestione dei segnali negli esempi precedenti è piuttosto rudimentale, con questa nuova funzione otteniamo molte più informazioni sul processo. #include #include #include #include #include #include <sys/types.h> <sys/wait.h> <signal.h> <unistd.h> <stdio.h> <ucontext.h> void sig_catch(int sig,siginfo_t *siginfo,struct ucontext *scp) ; int main(void) { struct sigaction act ; act.sa_flags = SA_SIGINFO ; act.sa_sigaction = sig_catch ; sigaction(SIGILL,&act,NULL); __asm__ ( ".long -1" ) ; return 0 ; } void sig_catch(int sig,siginfo_t *siginfo,struct ucontext *scp) { printf ("\nIstruzione Illegale indirizo : <0x%x> \n",siginfo->si_addr); exit(1) ; } sigation : questa funzione serve ad installare una funzione handler per il segnale signum. Occorre definire una struttura sigaction, così definita : struct sigaction { } void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; int sa_flags; void (*sa_restorer)(void); Il prototipo di funzione handler corrisponde al prototipo della funzione evidenziata in grassetto, quella precedente viene mantenuta per motivi di compatibilità. # man sigaction Esempio di utilizzo Sig Action : #include <stdio.h> #include <signal.h> #include <ucontext.h> void my_signal_handler(int sig, siginfo_t *sigInfo, struct ucontext *scp); int main(void) { struct sigaction sa; int i, j; sa.sa_flags sa.sa_sigaction = SA_SIGINFO ; = my_signal_handler; sigaction(SIGFPE, &sa, NULL); j = 10; i = 0; i = j / i; return 0 ; } void my_signal_handler(int sig, siginfo_t *sigInfo, struct context *scp) { printf("siginfo->si_code = %d\n", sigInfo->si_code); printf("siginfo->si_addr = %x\n", sigInfo->si_addr); printf("ss_sp = %d\n", scp->uc_stack.ss_sp); printf("FPE_INTDIV = %d\n", FPE_INTDIV); exit(0); } Attraverso sa_flags, ne definiamo il comportamento : SA_NOCLDSTOP If signum is SIGCHLD, do not receive notification when child processes stop (i.e., when they receive one of SIGSTOP, SIGTSTP, SIGTTIN or SIGTTOU) or resume (i.e., they receive SIGCONT) (see wait(2)). SA_NOCLDWAIT (Linux 2.6 and later) If signum is SIGCHLD, do not transform children into zombies when they terminate. See also waitpid(2). SA_RESETHAND Restore the signal action to the default state once the signal handler has been called. SA_ONESHOT is an obsolete, non-standard synonym for this flag. SA_ONSTACK Call the signal handler on an alternate signal stack provided by sigaltstack(2). If an alternate stack is not available, the default stack will be used. SA_RESTART Provide behaviour compatible with BSD signal semantics by making certain system calls restartable across signals. SA_NODEFER Do not prevent the signal from being received from within its own signal handler. SA_NOMASK is an obsolete, nonstandard synonym for this flag. SA_SIGINFO The signal handler takes 3 arguments, not one. In this case, sa_sigaction should be set instead of sa_handler. (The sa_sigaction field was added in Linux 2.1.86.) Attraverso siginfo_t possiamo ottenere ulteriori informazioni : siginfo_t { int int int pid_t uid_t int clock_t clock_t sigval_t int void * void * int int } si_signo; si_errno; si_code; si_pid; si_uid; si_status; si_utime; si_stime; si_value; si_int; si_ptr; si_addr; si_band; si_fd; /* /* /* /* /* /* /* /* /* /* /* /* /* /* Signal number */ An errno value */ Signal code */ Sending process ID */ Real user ID of sending process */ Exit value or signal */ User time consumed */ System time consumed */ Signal value */ POSIX.1b signal */ POSIX.1b signal */ Memory location which caused fault */ Band event */ File descriptor */ FIFO Un altro meccanismo di comunicazione tra i processi è rappresentato dalla fifo. Le fifo sono strutture del kernel per la comunicazione tra processi, ma a differenza delle pipe che possono essere solo utilizzate dal processo figlio, queste sono accessibile tramite inode, che risiede nel filesystem. Come per le pipe, nelle fifo i dati transiteranno attraverso un' apposito buffer nel kernel, senza tuttavia transitare dal filesystem. L'inode allocato dal FS (File System) serve solo a fornire un punto di riferimento tra i processi. Ricordo che per la comunicazione dei processi, le operazioni devono essere atomiche, questo avviene affinchè non si supera la dimensione del buffer messo a disposizione, PIPE_BUF . Nel nostro esempio scriveremo 2 file. Il primo client che ha la funzione di passare alcuni argomenti al processo server che fornirà la risposta attraverso le fifo. A fine semplicistico, il server otterrà i dati che gli servono da un file conosciuto /tmp/domanda ed inoltrerà le informazioni nel file /tmp/risposta Ovviamente con qualche istruzioni in più è possibile memorizzare nella fifo /tmp/domanda il nome del file fifo di risposta, questo sarebbe molto utile in quanto nella comune programmazione abbiamo diversi client che si affacciano su di un unico server. Il programma server una volta lanciato diventerà un 'demone' in attesa delle informazioni del programma client. Commento e listato, programma server : .file "pipes.c" .section .rodata.str1.1,"aMS",@progbits,1 .LC0: .string "/tmp/domanda" .globl fd # alloca un file descriptor (fd) : 4 byte .data # si riferisce a “/tmp/domanda” .align 4 .type fd, @object .size fd, 4 fd: .long .LC0 .section .rodata.str1.1 .LC1: .string "/tmp/risposta" .globl fr .data .align 4 .type fr, @object .size fr, 4 fr: .long .LC1 .section .rodata .LC2: .string "123" .string "" .text .p2align 4,,15 # alloca un file descriptor (fr) : 1 long # si riferisce a “/tmp/risposta” ################################### main .globl main .type main: pushl movl movl pushl subl xorl main, @function %ebp $402, %ecx %esp, %ebp %ebx $20, %esp %ebx, %ebx # Crea una fifo relativamente al file /tmp/domanda # quindi il server è in ascolto su questo file movl andl movl movl call fd, %edx $-16, %esp %ecx, 4(%esp) %edx, (%esp) mkfifo # fd "/tmp/domanda" # O_RDONLY 0622 # fd "/tmp/domanda" # mkfifo ( fd,0622) # Il programma server viene messo in background movl movl call %ebx, 4(%esp) $0, (%esp) daemon # 0 # 0 # daemon (0,0) ; # queste due routine servono ad aprire il letura/scrittura # la fifo, per evitare EOF movl xorl movl movl call fd , %ecx, %ecx, %edx, open %edx %ecx 4(%esp) (%esp) movl movl movl movl call fd, %edx $1, %eax %eax, 4(%esp) %edx, (%esp) open # RD_ONLY = 0 # fd # fifoserver = open (fd,RD_ONLY ); # WR_ONLY = 1 # fd # fifoserver = open (fd,WR_ONLY ) # questo è il ciclo principale del programma # apre il file di risposta per scriverci dentro # alcune informazioni, nel nostro caso “123” .L5: .p2align 4,,15 # while(1) { movl movl movl movl call fr, %ebx $1, %edx %edx, 4(%esp) %ebx, (%esp) open movl movl movl movl movl movl call %eax, %ebx $.LC2, %edx $4, %eax %eax, 8(%esp) %edx, 4(%esp) %ebx, (%esp) write # # # # # movl call %ebx, (%esp) close # fifoclient # close ( fifoclient ) ; # RD_ONLY = 1 # fr "/tmp/risposta" # fifoclient = open 4 "1 "123\0" fifoclient write ( fifoclient , "123\0" ,4 ) jmp .L5 # } .size main, .-main .section .note.GNU-stack,"",@progbits .ident "GCC: (GNU) 3.3.6 (Debian 1:3.3.6-7)" Programma lato client : .file "pipec.c" .section .rodata.str1.1,"aMS",@progbits,1 .LC1: .string "/tmp/risposta" .LC0: .string "/tmp/domanda" .LC2: .string "<file di risposta>" .LC3: .string "\n<<%s>>\n" .text .p2align 4,,15 .globl main .type main, @function main: pushl %ebp movl %esp, %ebp pushl %edi pushl %esi pushl %ebx # ascolta per la fifo /tmp/risposta subl $4204, %esp # 4096 alloca buffer (PIPE_BUF) movl andl movl leal movl call $402, %ebx $-16, %esp %ebx, 4(%esp) -4200(%ebp), %ebx $.LC1, (%esp) mkfifo # 0622 # 2° parametro : 622 # 1° parametro # crea la fifo : "/tmp/risposta" # apre il file fifo di domanda movl movl movl call $.LC0, (%esp) $1, %ecx %ecx, 4(%esp) open # /tmp/domanda # WR_ONLY movl movl call $.LC2, (%esp) %eax, %edi strlen "<file di risposta> # open ( fd, WR_ONLY ) ; # %eax = strlen (fifoname) # scrive nel file di domanda movl incl movl movl movl call %edi, (%esp) %eax $.LC2, %edx %edx, 4(%esp) %eax, 8(%esp) write # 1° fifo server # 2° <fifo name> # 3° lunghezza stringa # write ( fifoserver,fifoname,"..." ) ; # chiude la scrittura sulla fifo di domanda movl call %edi, (%esp) close # fifo server # close ( fifoserver) ; # apre la fifo per ottenere la risposta movl xorl movl call $.LC1, (%esp) %eax, %eax %eax, 4(%esp) open # 1° /tmp/risposta # 2° RD_ONLY # fifoclient = open ( fr,O_RD_ONLY ) ; # leggi per un massimo del PIPE_BUf, (operazioni atomiche) movl movl movl movl movl call %eax, (%esp) %eax, %esi $4096, %edx %edx, 8(%esp) %ebx, 4(%esp) read # 1° %eax=fifoclient # 3° sizeof ( PIPE_BUF) # 2° # read(fifoclient,buffer,sizeof(buffer) # visualizza il risultato, inviato dal server movl movl call %ebx, 4(%esp) $.LC3, (%esp) printf # printf ("<<%s>>",buffer); # chiude tutti i file e le fifo movl call %esi, (%esp) close # close (fifoclient) movl call %edi, (%esp) close # close (fifoserver) movl call $.LC2, (%esp) unlink # unlink ( fifoname ) ; leal xorl -12(%ebp), %esp %eax, %eax popl %ebx popl %esi popl %edi popl %ebp ret .size main, .-main .section .note.GNU-stack,"",@progbits .ident "GCC: (GNU) 3.3.6 (Debian 1:3.3.6-7)" Memoria Condivisa Un ultimo aspetto della comunicazione, che prendo in considerazione è quello della memoria condivisa. Permettere cioè a più programmi di poter condividere un segmento di memoria. Sarebbe utile approfondire l'argomento con i semafori al fine di bloccare l'acceso alla risorsa, ma tratto molto velocemente questo argomento, tutt'al più ritornerò in maniera più approfondita nel volume 2. La memoria condivisa è il terzo oggetto introdotto da SysV IPC per la comunicazione tra programmi. Possiamo basarci su tre distinte funzioni che allocano relativamente ad una chiave una struttura dati, ne ricercano la presenza in memoria e quindi una volta che non serve più, la rimuovono. Un punto fondamentale, che non tratto, è la sincronizzazione tra i processi. In quanto se un processo sta leggendo da tale area, l'altro ovviamente non deve scrivere nella medesima. A ciascun segmento di memoria condivisa viene associata una struttura dati : struct shmid_ds { struct ipc_perm size_t time_t time_t time_t pid_t pid_t shmatt_t }; shm_perm ; shm_segsz ; shm_atime ; shm_dtime ; shm_ctime ; shm_cpid ; shm_lpid ; shm_nattch ; /* /* /* /* /* /* /* /* Ownership and permissions */ dimensioni in byte del seg. */ Last attach time */ Last detach time */ Last change time */ PID of creator */ PID of last stmat()/shmdt() */ No. of current attaches */ ovviamente come le risorse in linux anche la memoria gode di determinati permessi : struct ipc_perm { key_t key ; uid_t uid ; gid_t gid ; uid_t cuid ; gid_t cgid ; unsigned short mode ; }; unsigned short seq ; /* /* /* /* /* /* Key supplied to shmget() */ Effective UID of owner */ Effective GID of owner */ Effective UID of creator */ Effective GID of creator */ Permissions + SHM_DEST and SHM_LOCKED flags */ /* Sequence number */ Come per le code, i messaggi e gli insiemi di semafori (non trattai), anche per i segmenti di memoria esistono dei limiti imposti dal sistema. Alcuni di questi limiti sono accessibili/modificabili attraverso sysctl o scrivendo direttamente nei rispettivi file /proc/sys/kernel/. La struttura della memoria può essere vista in questo modo dopo l'allocazione di un segmento di memoria condivisa : ENVIRONMENT 0xC000000C STACK Memoria Condivisa 0x40000000 .HEAP 0x08xxxxxxx .BSS .DATA .TEXT 0x080000000 Di seguito riporto le tre funzioni, scritte in 'C' per maggior chiarezza di come, allocare, ricercare e cancellare il segmento condiviso : #include #include #include #include #include #include #include #include #include #include <sys/shm.h> <sys/types.h> <sys/stat.h> <stdio.h> <fcntl.h> <signal.h> <unistd.h> <sys/mman.h> <string.h> <errno.h> /* SysV IPC shared memory declarations */ /* standard I/O functions */ /* signal handling declarations */ #include "macros.h" void * ShmCreate(key_t ipc_key, int shm_size, int perm, int fill) { void * shm_ptr; int shm_id; /* ID of the IPC shared memory segment shm_id = shmget(ipc_key, shm_size, IPC_CREAT|perm); /* get shm ID if (shm_id < 0) { return NULL; } shm_ptr = shmat(shm_id, NULL, 0); /* map it into memory if (shm_ptr < 0) { return NULL; } memset((void *)shm_ptr, fill, shm_size); /* fill segment return shm_ptr; } */ */ */ */ le subroutine sono piuttosto esplicative, quindi non mi soffermerò su esse, tuttavia voglio farvi notare che per allocare un segmento di memoria condivisa, necessiterò di una chiave univoca key_t, che mi servirà poi per ricercare il segmento. IPC_CREAT|perm = dice di creare un blocco di memoria con i rispettivi permessi. void * ShmFind(key_t ipc_key, int shm_size) { void * shm_ptr; int shm_id; /* ID of the SysV shared memory segment */ shm_id = shmget(ipc_key, shm_size, 0); /* find shared memory ID */ if (shm_id < 0) { return NULL; } shm_ptr = shmat(shm_id, NULL, 0); /* map it into memory */ if (shm_ptr < 0) { return NULL; } return shm_ptr; } ritorna l'indirizzo del segmento di memoria condiviso shm_id = shmget(ipc_key, shm_size, 0); int ShmRemove(key_t ipc_key, void * shm_ptr) { int shm_id; /* ID of the SysV shared memory segment */ /* first detach segment */ if (shmdt(shm_ptr) < 0) { return -1; } /* schedule segment removal */ shm_id = shmget(ipc_key, 0, 0); /* find shared memory ID */ if (shm_id < 0) { if (errno == EIDRM) return 0; return -1; } if (shmctl(shm_id, IPC_RMID, NULL) < 0) { /* ask for removal */ if (errno == EIDRM) return 0; return -1; } return 0; } Ancora riporto i due programmi, il server che allocala la memoria condivisa e aspetta che una variabile flag venga modificata dall'esterno per poter visualizzare la struttura dati ed un programma client che accede alla memoria condivisa e ne modifica i valori. Programma server : #include #include #include #include #include #include #include #include #include #include <sys/shm.h> <sys/types.h> <sys/stat.h> <stdio.h> <fcntl.h> <signal.h> <unistd.h> <sys/mman.h> <string.h> <errno.h> /* SysV IPC shared memory declarations */ /* standard I/O functions */ /* signal handling declarations */ void * ShmCreate(key_t ipc_key, int shm_size, int perm, int fill) { void * shm_ptr; int shm_id; /* ID of the IPC shared memory segment shm_id = shmget(ipc_key, shm_size, IPC_CREAT|perm); /* get shm ID if (shm_id < 0) { return NULL; } shm_ptr = shmat(shm_id, NULL, 0); /* map it into memory if (shm_ptr < 0) { return NULL; } memset((void *)shm_ptr, fill, shm_size); /* fill segment return shm_ptr; }; /****************************************/ /* QUESTA è LA NOSTRA STUTTURA DATI */ /****************************************/ struct ShareData { int flag ; int x ; int y ; } *pShareData ; /****************************************/ /* QUESTA è LA NOSTRA chiave */ /****************************************/ key_t key ; */ */ */ */ int main ( void ) { /* crea 4096 byte memoria condivisa permessi rwx, inizializza */ key = ftok ("Claudio Daffra",1); pShareData = ShmCreate ( key, 4096, 0666,0 ); /* inizializza il segmento con la nostra struttura */ memset ( pShareData , 0 , sizeof(struct ShareData) ) ; puts ( "\nWaiting for flag ...\n"); while(1) { if ( pShareData->flag == 1 ) { printf ( "\n Flag On : x*y = %d\n",pShareData->x * pShareData->y ) ; pShareData->flag = 0 ; /* ripristina lo stato di flag */ } } return 0 ; Il programma principale del server ha come unico scopo quello di visualizzare il prodotto di X*Y della nostra struttura quando la variabile flag è uguale a1. Programma client : #include #include #include #include #include #include #include #include #include #include <sys/shm.h> <sys/types.h> <sys/stat.h> <stdio.h> <fcntl.h> <signal.h> <unistd.h> <sys/mman.h> <string.h> <errno.h> /* SysV IPC shared memory declarations */ /* standard I/O functions */ /* signal handling declarations */ void * ShmFind(key_t ipc_key, int shm_size) { void * shm_ptr; int shm_id; /* ID of the SysV shared memory segment */ shm_id = shmget(ipc_key, shm_size, 0); /* find shared memory ID */ if (shm_id < 0) { return NULL; } shm_ptr = shmat(shm_id, NULL, 0); /* map it into memory */ if (shm_ptr < 0) { return NULL; } return shm_ptr; } /****************************************/ /* QUESTA è LA NOSTRA STUTTURA DATI */ /****************************************/ struct ShareData { int flag ; int x ; int y ; } *pShareData ; int main ( void) { key_t key ; key = ftok ("Claudio Daffra",1); /* ricerca il blocco di memoria condiviso */ pShareData = ShmFind ( key, 4096) ; pShareData->x = 2 ; pShareData->y = 3 ; pShareData->flag = 1 ; return 0 } ; p.s. Non dimenticatevi di rimuovere il blocco di memoria condiviso !!! CAPITOLO 23 Networking Networking Questo capitolo tratta, della programmazione di rete, meglio cerca di introdurre il lettore nell'ambito di concetti generali, che successivamente potrà sviluppare in maggior dettaglio. L'unica differenza tra un'applicazione che opera nella rete ed un programma è che la prima interagisce con altri programmi, di applicativi diversi e di sistemi operativi diversi, tramite opportuni protocolli standard. Negli esempi visti fino ad adesso, è possibile creare diversi programmi, che interagiscono tra di loro ma nell'ambito del proprio sistema operativo con le proprie regole, nella programmazione di rete ci troviamo di fronte ad interagire non solo con differenti programmi, ma con differenti sistemi operativi che operano in modo dissimile dal nostro. Modello Client/Server Client e Server è l'architettura fondamentale su cui si basa gran parte della programmazione di rete in ambiente linux, e in unix in generale. Questo modello si basa concettualmente su servizi che sono i server che ricevono le richieste dai vari programmi i client. Quindi di norma un server deve essere in grado di rispondere a più client. Se ricordate l'esempio con le fifo notavate che il cliente interrogava il server con un dato file, se non all'interno del file aggiungiamo informazioni, ad esempio un file di risposta, il server potrà gestire queste nuove informazioni come fifo per rispondere su un a fifo personale al client, cosicchè molti client avranno le rispettive fifo. Modello Peer to Peer Nel precedente modello, a farla da padrone in parole povere era il server, quest' ultime architetture a differenza di quelle precedente si basano sul fatto di non aver nessun programma centrale come riferimento, ma entrambi i programmi svolgono funzioni di client/server. (bittorrent) Ogni programma quindi invia e riceve richieste. Modello three-tier Questo è un'estensione del modello client/server, in particolare si è visto in internet il crescere di una moltitudine di servizi tra loro integrati e quindi una crescita in complessità. Per esempio integrazione servizi web con database e quindi le pagine vengono costruite dinamicamente in relazione al database. Nell'architettura client/server il grosso collo di bottiglia è lo scontrarsi di molti client con un unico server. Quindi si è pensato di ridistribuire i servizi tra più server identici, mantenendo sostanzialmente inalterata la struttura originaria dei client / server. Protocolli Nella parte introduttiva relativa a questo capitolo abbiamo accennato all'eterogeneità per quanto riguarda la gestione dei dati da e per programmi diversi tra loro e scritti per differenti sistemi operativi. L'interscambio corretto di dati avente attraverso degli opportuni protocolli di comunicazioni. La tabella successiva illustra i due protocolli più diffusi : Il modello ISO/OSI si basa su 7 livelli ed il modello DoD (Department of Defense) quello di Arpa, che con ARPA-NET oggi è diventato INTERNET. OSI/ISO Livello 7 APPLICATIOIN Livello 6 PRESENTATION Livello 5 SESSION Livello 4 DoD Livello 4 APPLICATION HTTP, FTP,... TRANSPORT Livello 3 TRANSPORT TCP Livello 3 NETWORK Livello 2 NETWORK IP Livello 2 DATA LINK LINK Device Driver Livello 1 PHISICAL Livello 1 Scheda interfaccia TCP/IP Questo modello è molto semplice e strutturato i 4 livelli. Al livello base abbiamo la scheda di rete, o dispositivo elettronico che costituisce la parte fisica. Al livello successivo 1,abbiamo il livello di rete che si occupa di gestire l'instradamento dei pacchetti IP (protocollo non sicuro). Quindi al livello 3 quello di trasporto (TCP) abbiamo la garanzia dell'avvenuto scambio/trasmissione di dati. Infine quello dell'applicazione (4) che viene gestito dall'applicazione. Queste adottano determinati protocolli per lo scambio di dti (HTTP,SMTP,POP ). Il protocollo TCP/IP è un insieme di protocolli diversi che operano sui 4 livelli. Tra i vari protocolli troviamo : IPV4 : Internet Protocol Version 4, utilizza indirizzi di 32 bit, anni '80. IPV6 : Internet Protocol Version 6, utilizza indirizzi a 128 bit, anni '90. TCP : Trasmission Control Protocol, protocollo orientato ad un trasporto di dati affidabile. PPP : Point-to-Point Protocol, (livello 1) progettato per lo scambio di pacchetti su connessioni punto punto. SLIP : Permette di trasmettere un pacchetto attraverso una linea seriale. Prima abbiamo parlato di TCP e di protocollo sicuro, in effetti il protocollo IP (Internet Protocol), nasce per creare una interfaccia per lo scambio di dati tra le reti, indipendente dal substrato hardware stesso. Il compito del protocollo IP è quello di trasmettere i dati da un computer all'altro, con due caratteristiche essenziali : 1) Universal Addressing : Comunicazioni tra due stazioni remote, identificate univocamente da un indirizzo a 32 bit; 2) Best Effort : Viene assicurato il massimo impegno nella trasmissione, ma non c'è nessuna garanzia per i livelli superiori, ne per la consegna tantomeno per il tempo. Socket I socket sono uno dei principali meccanismi di comunicazione. Questo in sostanza costituisce un canale di comunicazione tra due processi. Tuttavia la differenza tra le pipe/fifo viste in precedenza è che i socket interagiscono su macchine con sistemi e architettura differenti. Per capire il funzionamento dei socket occorre aver presente il funzionamento dei protocolli di rete. Esaminiamo ora alcune funzioni per la gestione dei socket : #include <sys/socket.h> int socket ( int domain, int type, int protocol ) Questa funzione crea un socket e restituisce un file descriptor, oppure -1 in caso di fallimento. Esempio : sock_fd = socket( AF_INET, SOCK_STREAM, 0 ) ; a) domain : ci sono molti protocolli di comunicazione e quindi diversi tipi di socket che vengono classificati in quelli che vengono chiamati domini. La scelta di un dominio equivale a scegliere una famiglia di protocolli e viene effettuato attraverso l'argomento domain della funzione socket. Ciascun dominio ha un nome simbolico che inizia con PF_, (Protocol Familiy) che indentifica il formato degli indirizzi utilizzato nel dominio. Nella successiva tabella vengono elencati alcuni protocollisocket li potete traovare in : /etc/protocols ip icmp igmp ggp ipencap st tcp egp pup udp hmp xns-idp rdp iso-tp4 xtp ddp idpr-cmtp rspf vmtp ospf ipip encap 0 1 2 3 4 5 6 8 12 17 20 22 27 29 36 37 39 73 81 89 94 98 IP ICMP IGMP GGP IP-ENCAP ST TCP EGP PUP UDP HMP XNS-IDP RDP ISO-TP4 XTP DDP IDPR-CMTP RSPF VMTP OSPFIGP IPIP ENCAP # # # # # # # # # # # # # # # # # # # # # # internet protocol, pseudo prot. number internet control message protocol Internet Group Management gateway-gateway protocol IP encapsulated in IP (officially "IP") ST datagram mode transmission control protocol exterior gateway protocol PARC universal packet protocol user datagram protocol host monitoring protocol Xerox NS IDP "reliable datagram" protocol ISO Transport Protocol class 4 Xpress Transfer Protocol Datagram Delivery Protocol IDPR Control Message Transport Radio Shortest Path First. Versatile Message Transport Open Shortest Path First IGP Yet Another IP encapsulation Yet Another IP encapsulation ipv6 ipv6-route ipv6-frag ipv6-crypt ipv6-auth icmpv6 ipv6-nonxt ipv6-opts 41 43 44 50 51 58 59 60 IPv6 IPv6-Route IPv6-Frag IPv6-Crypt IPv6-Auth IPv6-ICMP IPv6-NoNxt IPv6-Opts # # # # # # # # IPv6 Routing Header for IPv6 Fragment Header for IPv6 Encryption Header for IPv6 Authentication Header for IPv6 ICMP for IPv6 No Next Header for IPv6 Destination Options for IPv b) type : la scelta dl dominio non comporta però lo stile di comunicazione, questo infatti dipenderà dal protocollo che si intenderà utilizzare, fra quelli disponibili nella famiglia. Alcuni stili : SOCK_STREAM : provvede ad un canale di trasmissione dati bidirezionale. SOCK_DGRAM : viene usato per trasmettere pacchetti di dati DATAGRAM di lunghezza massima prefissata. SOCK_SEQPACKET : canale di trasmissione dati bidirezionale, sequenziale ed affidabile. SOCK_RAW : prevede l'acceso a basso livello dei protocolli di rete. SOCK_RDM : prevede un canale di trasmissione dati affidabile. SOCK_PACKET : obsoleto. c) protocol : questo a meno che non si usa il tipo raw rimane sempre 0. LE PORTE Ogni processo locale che comunica con uno remoto viene identificato in una connessione TCP/IP tramite una porta. Una porta è rappresentata, all'interno di un pacchetto TCP o UDP , da un campo a 16 bit che può assumere un valore tra 0 e 65535. E' possibile suddivere i tipi di porte in tre categorie: Well Known Ports: il cui valore va da 0 a 1023 sono assegnate a specifici protocolli dalla Internet Assigned Number Authority (IANA ) Registered Ports: sono registrate a nome delle società che hanno sviluppato specifiche applicazioni; Dynamic and/or Private Ports: il cui valore va da 49152 a 65535, non sono gestite da nessun organo di controllo, e vengono assegnate dinamicamente, dal sistema operativo, quando un client si connette ad un host remoto; Ancora SOCKET La combinazione tra indirizzo IP, protocollo di trasporto e numero di porta prende il nome di Socket. Le condizioni per instaurare una connessione TCP sono due: – apertura passiva lato server, la quale indica al sistema operativo su quale porta vengono accettate le connessioni; – apertura attiva lato client, che richiede al sistema operativo l'assegnamento di una porta per connettersi all'host remoto; – WELL KNOWN PORTS Sebbene generalmente un'applicazione utilizzi solamente un protocollo tra TCP e UDP , vi sono dei casi come per esempio il protocollo DNS o altri in cui vengono utilizzati entrambi i protocolli. In quest'ultimo caso si avrà il medesimo numero di sia per quanto riguarda TCP che per quanto riguarda UDP. Di seguito alcune tra le Well Known Port (porte note) più comuni: Porte TCP 7 20 21 22 23 25 53 ECHO FTP DATA FTP SSH TELNET SMTP DNS - Servizio Echo; File Transfer Protocol Dati; File Transfer Protocol Controllo; Secure Shell Remote Login Protocol Telnet Protocol; Simple Mail Transfer Protocol; Server dei nomi di dominio; 67 BOOTPS 68 BOOTPC 80 HTTP 110 POP3 111 SUNRPC 113 AUTH 119 NNTP 137 NETBIOS-NS 138 NETBIOS-DGM 139 NETBIOS-SSN 143 IMAP 389 LDAP 443 HTTPS 515 PRINTER - (Dhcp) Bootstrap Protocol Server; (Dhcp) Bootstrap Protocol Client; Hypertext Transmission Protocol; Post Office Protocol 3; Sun RPC Portmap Servizio autenticazione; Network News Transfer Protocol; NETBIOS Name Service NETBIOS Datagram Service NETBIOS Session Service Internet Mail Access Protocol; Lightweight Directory Access Protocol; http protocol over TLS/SSL; Spooler; Porte UDP 7 ECHO 53 DNS 67 BOOTPS 68 BOOTPC 69 TFTP 111 SUNRPC 123 NTP 137 NETBIOS-NS 138 NETBIOS-DGM 139 NETBIOS-SSN 161 SNMP 162 SNMP 515 PRINTER - Servizio Echo; Server dei nomi di dominio; (Dhcp) Bootstrap Protocol Server; (Dhcp) Bootstrap Protocol Client; Trivial File Transfer Protocol; Sun RPC Portmap; Network Time Protocol; NETBIOS Name Service; NETBIOS Datagram Service; NETBIOS Session Service Simple Network Management Protocol (SNMP); TRAP Simple Network Management Protocol Trap; SERVIZI I servizi di rete si posizionano sopra i protocolli che fanno uso di IP. Questi sono elencati normalmente all'interno del file /etc/services. Questo file, in particolare, viene utilizzato per conoscere esattamente il numero di porta su cui normalmente si trova in ascolto un servizio determinato e quali tipi di protocolli vengono utilizzati da ogni servizio. /etc/services Si tratta del file contenente l'elenco dei nomi standard dei vari servizi di rete. Viene utilizzato in particolare da inetd, oltre che da altri programmi, per interpretare correttamente i nomi di tali servizi indicati nel suo file di configurazione /etc/inetd.conf. # # services # # # # # Version: # # Author: # This file describes the various services that are available from the TCP/IP subsystem. It should be consulted instead of using the numbers in the ARPA include files, or, worse, just guessing them. tcpmux echo echo discard discard systat daytime daytime netstat qotd chargen chargen ftp-data ftp telnet smtp time time rlp name whois domain domain mtp bootps bootpc tftp gopher rje finger http www link kerberos kerberos supdup hostnames iso-tsap x400 x400-snd csnet-ns pop-2 pop-3 pop sunrpc 1/tcp 7/tcp 7/udp 9/tcp 9/udp 11/tcp 13/tcp 13/udp 15/tcp 17/tcp 19/tcp 19/udp 20/tcp 21/tcp 23/tcp 25/tcp 37/tcp 37/udp 39/udp 42/udp 43/tcp 53/tcp 53/udp 57/tcp 67/udp 68/udp 69/udp 70/tcp 77/tcp 79/tcp 80/tcp 80/tcp 87/tcp 88/udp 88/tcp 95/tcp 101/tcp 102/tcp 103/tcp 104/tcp 105/tcp 109/tcp 110/tcp 110/tcp 111/tcp @(#)/etc/services 2.00 04/30/93 Fred N. van Kempen, <[email protected]> # rfc-1078 sink null sink null users quote ttytst source ttytst source mail timserver timserver resource nameserver nicname # resource location # usually to sri-nic # deprecated # bootp server # bootp client # gopher server ttylink kdc kdc hostname # www is used by some broken # progs, http is more correct # # # # Kerberos authentication--udp Kerberos authentication--tcp BSD supdupd(8) usually to sri-nic # ISO Mail # PostOffice V.2 # PostOffice V.3 # PostOffice V.3 sunrpc sunrpc sunrpc auth sftp uucp-path nntp ntp ntp netbios-ns netbios-ns netbios-dgm netbios-dgm netbios-ssn imap NeWS snmp snmp-trap exec biff login who 111/tcp 111/udp 111/udp 113/tcp 115/tcp 117/tcp 119/tcp 123/tcp 123/udp 137/tcp 137/udp 138/tcp 138/udp 139/tcp 143/tcp 144/tcp 161/udp 162/udp 512/tcp 512/udp 513/tcp 513/udp portmapper # RPC 4.0 portmapper UDP portmapper ident # RPC 4.0 portmapper TCP # User Verification usenet # Network News Transfer # Network Time Protocol # Network Time Protocol shell syslog printer talk ntalk efs route timed tempo courier conference netnews netwall uucp klogin kshell new-rwho remotefs rmonitor monitor pcserver mount pcnfs bwnfs kerberos-adm kerberos-adm kerberos-sec kerberos-sec kerberos_master kerberos_master krb5_prop listen nterm kpop ingreslock tnet cfinger nfs eklogin krb524 irc dos 514/tcp 514/udp 515/tcp 517/udp 518/udp 520/tcp 520/udp 525/udp 526/tcp 530/tcp 531/tcp 532/tcp 533/udp 540/tcp 543/tcp 544/tcp 550/udp 556/tcp 560/udp 561/udp 600/tcp 635/udp 640/udp 650/udp 749/tcp 749/udp 750/udp 750/tcp 751/udp 751/tcp 754/tcp 1025/tcp 1026/tcp 1109/tcp 1524/tcp 1600/tcp 2003/tcp 2049/udp 2105/tcp 4444/tcp 6667/tcp 7000/tcp cmd nbns nbns nbdgm nbdgm nbssn news comsat whod spooler router routed timeserver newdate rpc chat readnews # imap network mail protocol # Window System # BSD rexecd(8) # BSD rlogind(8) # BSD rwhod(8) # # # # # # # BSD rshd(8) BSD syslogd(8) BSD lpd(8) BSD talkd(8) SunOS talkd(8) for LucasFilm 521/udp too # experimental # -for emergency broadcasts # BSD uucpd(8) UUCP service # Kerberos authenticated rlogin cmd # and remote shell new-who # experimental rfs_server rfs # Brunhoff remote filesystem rmonitord # experimental # experimental # ECD Integrated PC board srvr # NFS Mount Service # PC-NFS DOS Authentication # BW-NFS DOS Authentication # Kerberos 5 admin/changepw # Kerberos 5 admin/changepw # Kerberos authentication--udp # Kerberos authentication--tcp # Kerberos authentication # Kerberos authentication # Kerberos slave propagation listener RFS remote_file_sharing remote_login network_terminal # Pop with Kerberos uucpd msdos # # # # # # transputer net daemon GNU finger NFS File Service Kerberos encrypted rlogin Kerberos 5 to 4 ticket xlator Internet Relay Chat # End of services. Di seguito includo a titolo esplicativo il sorgente 'C', del file TCP_daytime.c prelevato dall'archivio GaPiL, nel quale ringrazio l'autore per aver condiviso con la comunità la sua esperienza. Grazie simone Piccardi. La restante parte di codice in linguaggio macchina non la commento in quanto ritengo che siate in grado di ricavare da soli ogni informazione sul programma, altrimenti ritorna a leggere capitolo 1 se ancora questa spiegazione non è stata sufficiente, ahimè rimando ad una lettura di monografie certamente più autorevoli di questa ! #include #include #include #include #include <sys/types.h> /* predefined types */ <unistd.h> /* include unix standard library */ <arpa/inet.h> /* IP addresses conversion utiliites */ <sys/socket.h> /* socket library */ <stdio.h> /* include standard I/O library */ #define MAXLINE 80 /* Program begin */ int main(int argc, char *argv[]) { /* * Variables definition */ int sock_fd; int i, nread; struct sockaddr_in serv_add; char buffer[MAXLINE]; /* *********************************************************** * * Options processing completed * * Main code beginning * * ***********************************************************/ /* create socket */ if ( (sock_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { perror("Socket creation error"); return -1; } /* initialize address */ memset((void *) &serv_add, 0, sizeof(serv_add)); /* clear server address */ serv_add.sin_family = AF_INET; /* address type is INET */ serv_add.sin_port = htons(13); /* daytime port is 13 */ /* build address using inet_pton */ if ( (inet_pton(AF_INET, argv[optind], &serv_add.sin_addr)) <= 0) { perror("Address creation error"); return -1; } /* extablish connection */ if (connect(sock_fd, (struct sockaddr *)&serv_add, sizeof(serv_add)) < 0) { perror("Connection error"); return -1; } /* read daytime from server */ while ( (nread = read(sock_fd, buffer, MAXLINE)) > 0) { buffer[nread]=0; if (fputs(buffer, stdout) == EOF) { /* write daytime */ perror("fputs error"); return -1; } } /* error on read */ if (nread < 0) { perror("Read error"); return -1; } } /* normal exit */ return 0; .LC3: .LC4: .LC2: .LC1: .LC0: .file "daytime.c" .section .rodata.str1.1,"aMS",@progbits,1 .string "fputs error" .string "Read error" .string "Connection error" .string "Address creation error" .string "Socket creation error" .text .p2align 4,,15 .globl main .type main, @function main: pushl %ebp xorl %eax, %eax movl %esp, %ebp pushl %edi pushl %ebx subl $112 , %esp andl $-16 , %esp movl %eax , 8(%esp) movl $1 , %eax movl %eax , 4(%esp) movl $2 , (%esp) call socket testl movl js cld xorl leal movl leal rep stosl movl movl leal movw movl movw movl movl movl call %eax, %eax %eax, %ebx .L15 %eax -24(%ebp) $4 -20(%ebp) , , , , %eax %edi %ecx %edx %edx 12(%ebp) -24(%ebp) $2 optind $3328 (%eax,%edx,4) $2 %ecx inet_pton , , , , , , , , , 8(%esp) %eax %edi -24(%ebp) %edx -22(%ebp) %ecx (%esp) 4(%esp) .L7: .L14: .L1: .L18: .L19: .L17: .L16: .L15: testl %eax jle .L16 movl %edi movl $16 leal -104(%ebp) movl %eax movl %ebx call connect testl %eax, %eax js .L17 .p2align 4,,15 , %eax , , , , , 4(%esp) %eax %edi 8(%esp) (%esp) movl movl movl movl call %edi $80 %edx %ebx read , , , , 4(%esp) %edx 8(%esp) (%esp) testl jle movb movl movl movl call %eax, %eax .L18 $0 stdout %edi %ecx fputs , , , , -104(%eax,%ebp) %ecx (%esp) 4(%esp) incl jne movl %eax .L7 $.LC3, (%esp) call movl perror $-1, %eax leal popl popl popl ret -8(%ebp), %esp %ebx %edi %ebp jl xorl jmp .L19 %eax, %eax .L1 movl jmp $.LC4, (%esp) .L14 movl jmp $.LC2, (%esp) .L14 movl jmp $.LC1, (%esp) .L14 movl $.LC0, (%esp) jmp .L14 .size main, .-main .section .note.GNU-stack,"",@progbits .ident "GCC: (GNU) 3.3.5 (Debian 1:3.3.5-13)" CAPITOLO 24 X86-64 X86-64 Il set delle istruzioni a 32 bit di intel IA32 (ISA), conosciuto anche con il nome "x86", è stato definito nel lontano 1985 con l'introduzione dei processori i386 che hanno esteso le istruzioni a 16 bit originalmente definito col processore 8086. Anche se sono state introdotte molte istruzioni da questa data nel set originario, molti compilatori le hanno continuato ad utilizzare per mantenere la compatibilità con i vecchi programmi. Oggi assistiamo all'introduzione dell'architettura x86-64 originariamente sviluppata da Advanced Micro Device (AMD) e così chiamata supportata dai processo AMD64 e EM64T. Benchè anche questa architettura è retrocompatibile, sono state introdotte molte nuove caratteristiche che possono essere utilizzate solo su questi processori. Fatto l'esempio dell'utilizzo di 16 registri generali anzichè otto. Quindi anzichè passare i parametri attraverso lo stack, è stato fatto molto lavoro al fine di utilizzare nel kernel questi registri come passaggio dei parametri (vedi system call), riducendo il numero di volte in cui la memoria viene letta/scritta. Da piu' d venti anni dall'introduzione dell' i386,le possibilità offerte dai microprocessori sono cambiate drammaticalmente. Nel 1985 un pc poteva avere 1 megabyte di rame circa 50 megabyte di hd, la velocità dei processori si aggirava attorno alle freq. di 5 / 10 megaherts (1 mips). Oggi un tipico high-end system ha circa 1 giga di ram, 500 g/b di storage ed all'incirca 4 g/hz di clock running a 5 billioni di i/s. Tuttavia il codice dei computer di oggi è ancora basato a quella di 20 anni prima. L'architettura a 32 bit ha posto ai giorni nostri dei seri limiti, per quanto riguarda la memoria non è possibile indirizzare oltre i 4 giga di ram, (32 bit), quindi questo è stata una grossa limitazione per i programmi che fanno largo uso di grosse strutture dati. Si è ovviato a tale problema scrivendo porzioni sul disco. L'introduzione dei 64 bit la possiamo già vedere dal 1992 con la digital e nel '95 con Sun e con il suo SPARC. Per quanto riguarda intel il primo processore con architettura a 64 bit non apparve prima del 2001 sviluppato con Hewlett-Packard (Very Large Instruction Word VLIW). Tuttavia l' introduzione di tale architettura da parte di intel non ha goduto subito di notorietà in quanto benchè potesse eseguire codice retrocompatibile, le attuali architetture a 32 bit risultavano poco più veloci .Anche Amd ha introdotto il suo processore x86-64 che sta a significare un'estensione di questa architettura rispetto alla precedente con alcune nuove caratteristiche ed ottimizzazioni. Questo capitolo illustrerà brevemente le caratteristiche di quest' architettura, senza scendere nei dettagli e senza particolari esempi. Una trattazione più approfondita nel vol. 2. L'architettura x86-64 è essenzialmente un' estensione della precedente, permettendo di mantenere la retro-compatibilità. Possiamo distinguere 4 modalità di funzionamento per questo processore : – – – – reale virtuale protetta long (16 bit) ; (8086) ; (32bit) ; (64bit) . Il funzionamento si avvale di un modalità detta 'compatibility mode' che permette di eseguire sui sistemi a 64 bit le applicazione x86 a 16 e 32 bit. Ed un 64-bit-mode appunto per le applicazioni a 64bit. Il legacy mode è invece la modalità di default in cui il processore si comporta esattamente come per il processore ia32 permettendo l'esecuzione di applicazioni e sistemi operativi a 16-bit e 32bit. – – – xmm0 ... xmm15 r0 ... r15 x87 128 bit 64 bit 80 bit – EIP 64 bit Gestione della memoria Per quanto riguarda la modalità legacy non ci sono novità, in effetti nelle gestione reale, virtuale e protetta la gestione rimane la stessa nel long mode invece abbiamo uno spazio di indirizzamento virtuale complessivo di 64 bit. Un'importante novità introdotta con la 64-bit-mode è l'eliminazione dell'implementazione hardware della segmentazione (che invece è presente nelle altre modalità). Nella forma generale l'istruzione prende questa forma : Legacy – REX - opcode – Mod RM SIB Disp (1,2,4) , Imm ( 1,2,4) prefix prefix l'unica differenza rispetto alla precedente architettura è l'introduzione del byte REX che serve per scegliere la dimensione degli operandi (32, 64 bit). Questo byte è stato ricavato sacrificando gli opcode di due istruzioni, che non sono quindi più utilizzabili nei programmmi a 64 bit. Ancora la possibilità di indirizzare i dati rispetto all' IP invece che alla base del segmento corrente. Questo rende non più necessario l'aggiustamento in fase di caricamento del programma,. degli indirizzi dei dati la cui posizione è indipendente da quella del codice (dati statici). Uno sguardo d'insieme Da questa tabella è possibile evincere la dimensioni dei tipi in relazione al compilatore gas. Dichiarazione C Intel data tyep GAS suffix char short int unsigned long int unsigned long char * float double long double byte word dword dword quad word quad word quad word single precision double precision extended precision Size(byte) b w l l q q q s d t 1 2 4 4 8 8 8 4 8 10 Alcune caratteristiche : - da qui si evince che in puntatori e gli interi ora sono lunghi 8 byte; (codice poco più grande), ma da la possibilità di accedere a 16 exabyte. Il set dei registri generali è passato da 8 a 16 ; molte delle routine che mantenevano i dati nello stack ora possono passarli attraverso i registri. le operazioni condizionali sono implementate utilizzando i conditional move ; le istruzioni in virgola mobile sono implementate utilizzando direttamente i registri. Un primo esempio long int esempio( long int *xp, lont int y ) [ long int t = *xp + y ; *xp = t ; return t ; ] > gcc -o2 -S -m32 code.c -o code.s Questo comando dice al compilatore di generare codice compatibilie per i 32 bit. come potete vedere esempio: pushl movl movl movl addl movl %ebp %esp 8(%ebp) (%edx) 12(%ebp) %eax ,%ebp ,%edx ,%eax ,%eax ,(%edx) leave ret In IA32 gli argomenti sono passati sullo stack 8(%ebp) [xp] and 12(%ebp) [y] ora compiliamo lo stesso esempio così : > gcc -o2 -S -m64 code.c -o code.s l'argomento è passato direttamente nel registro. esempio: addq (%rdi) , movq %rsi , movq %rsi , ret %rsi # add *xp a y and get t %rax # set t come indirizzo di ritorno (%rdi) # lo memorizza nella locazione di memoria come potete notare nella codifica a 64 bit : - non viene utilizzato lo stack per passare i parametri ma direttamente i registri ; - il suffisso da 'l' passa a 'q' ; - i nomi dei registri cambiano %rsi (registri indice). Nel commento dei due codice occorre evidenziare che nell'architettura IA32 ci sono 8 istruzioni e 7 fanno riferimento alla memoria nell'x86-64 solo 4 istruzioni con 3 referenti alla memoria, questo produce codice più compatto e veloce. (vedi però le accresciute dimensioni dei puntatori e interi). Ancora possiamo notare che nell'architettura a 32 bit occorrono 17 cicli di clock per completare le operazioni, mentre nell'architettura a 64 bit ne occorrono 9 con un incremento di prestazioni di 1.3 / 1.4 volte. Accedere alle informazioni Caratteristiche dell' x86-64 : – i numeri dei registri generali sono passati da 8 a 16; tutti i registri sono lunghi 64 bit e sono un'estensione dell'architettura 32 bit 64 bit 32 bit 16 bit 8 bit rax eax ax al rbx ebx bx bl rcx ecx cx cl rdx edx dx dl rsi esi si sil rdi edi di dil rsp esp sp spl rbp ebp bp bpl r8 r8d r8w r8b r9 r9d r9w r9b r10 r10d r10w r10b r11 r11d r11w r11b r12 r12d r12w r12b r13 r13d r13w r13b r14 r14d r14w r14b r15 r15d r15w r15b Come potete, notare oltre ai consueti registri, sono presenti il set di istruzioni dei registri generali che va da 8 a 15, per un totale di 16 e alcuni registri per accedere direttamente alla parte bassa dello stesso (ex. bpl). Nella sua sintassi generale il registro a 64 bit : [r] (num) (size) r 1 w r = registro a 64 bit ; num = registro generale da 0 a 15 ; size= puo' assumere i seguenti valori Byte,Word,Double Word (b,w,d); Qui fornisco alcune istruzioni di uso generale : data movement instruction movq moaabsq movslq movsbq movzbq move move move move move quad qord quad word sign extended dw sign extended byte zero extended byte istruzioni arimetiche / logiche leaq incq decq negq notq addq subq imulq load effective address incrementa decrementa nega complemento add sottrai moltiplica xorq orq and or esclusivo or and salq shlq sarq shrq shift arimetic left shit left shift arimetic right shit right imulq mulq ctlq idivq divq signed full multiply unsigned full multiply convert %eax to quad qord signed diveie unsigned divide vediamo un piccolo esempio i pratica, nel nostro esempio si assume che int=32 bit e long=64 bit : esempio 1 : long int func ( int x, int y) [ long int t1 = (long) x+y ; long int t2 = (long) (x+y) ; ] // 64 bit // 32 bit return t1 ! t2 ; gfun: movslq%edi,%rax movslq%esi,%rdx # converte x a long %edi=x # converte y a long %esi=y addl addq # addizione a 32 bit # t1 64 bit addition %esi,%edi %rdx,%rax movslq %edi,%rdi # signed extend to get t2 orq ret # return t1 | t2 ; %edi,%rax esempio 2 : return a*b + c*d ; # argomenti %edi=a, %sil=b, %rdx=c, %ecx=d movslq movsbl imulq imull leal ret %ecx %sil %rdx %edi (%rsi , %rcx , %esi ,%rcx # -> %rcx *= %rdx ,%esi # -> %esi *= %edi ,%rcx),%eax # -> %eax = %rsi + %rcx Istruzioni di controllo Le istruzioni di controllo ed i metodi implementati sono sostanzialmente gli stessi di quelli dell'architettura IA32. Due sole nuove istruzioni cmpq e testq sono state aggiunte. Istruzioni di mov condizionale Dal PentiumPro in poi sono state addizionate al set di istruzioni, quelle di mov condizionale, peraltro poco usate, che come dice l'istruzione stessa copiano un valore dalla sorgente alla destinazione se si verifica intrinsecamente una determinata condizione. ccMov è ora come "copia se" : istruzione condizione descrizione cmove cmovne cmovs cmovns cmovg/cmovnle cmovge/cmovnl cmovl/cmovnge cmovle/cmovng cmova/cmovnbe cmovae/cmovnb cmovb/cmovnae cmovbe/cmova zf af sf sf zf (SF OF) SF OF SF OF ZF CF ZF CF CF CF ZF equa/zero Not euql/Not zero Negative Non Negative Greater (signed >) SF OF Greter or equal Less (signed <) Less or equal (signed <=) Above (unsigned >) Above or equal (unsigned >=) below (unsigned <) below or euqal (unsigned <) esempio 1 : max: cmpl %esi , cmovge %edi , %edi %esi movl ret %eax %esi , Cicli Generalmente nell'IA32 quando un compilatore deve codificare in linguaggio macchina, si avvale dei template per le strutture riconosciute, molto codice viene poi ricondotto alla stessa struttura per esempio i cicli do-while, while e for vengono ricondotti tutti alla stessa struttura. Contrariamente nell'I64 troviamo una grande variata di template per i loop : esempio int fact_dw( int x ) [ int result = 1 ; do [ # fact_dw: # movl $1,%eax .l2 result *= x ; x-- ; ] while (x>0) ; # imul %edi,%eax # decl %edi # testl %edi,%edi # jg .l2 return result ; # rep ret Avete visto bene ! "rep ret", noi possiamo vedere che il ciclo termina con un in' approppriata istruzione almeno se ci riferiamo all'istruzione rep solo per le stringhe. La risposta è da andarla a cercare nelle linee guida di AMD, questo viene raccomandato per evitare che l'istruzione ret sia il target di ritorno di un'istruzione condizionale. Nel nostro caso se l'istruzione "jg " . Procedure Ho già avuto modo di mostrare brevemente come, l'implementazione delle chiamate alle procedure sia diverso nell'architettura ia64. Raddoppiando il numero di registri generali messi a disposizione, le variabili passati alla funzione non sono più così indipendenti dallo stack. - L'istruzione "call" memorizza un indirizzo di ritorno a 64 bit ; - molte funzione non richiedono lo stack frame ; - non c'è il frame pointer. Invece il riferimento alle locazione dello stack sono fatte dallo stack pointer ; - come nell'ia32 alcuni registri sono designati come "call-save" register, questi devono essere ripristinati da una subroutine che li modifichi. Stack Il registro %rsp mantiene un puntatore alla cima dello stack. Dissimilmente dall'ia32 non c'è un registro di frame pointer (%rbp), questo è disponibile per l'uso generale. Vedremo in seguito con qualche esempio. Passare gli argomenti Fino a 6 argomenti possono essere passati ai registri, questi vengono utilizzati in uno specifico ordine e le dimensioni del parametro fanno riferimento al tipo di registro. I restanti vengono individuati sullo stack. numero argomenti 1 2 3 4 5 6 64 32 16 8 %rdi %rsi %rdx %rcx %r8 %r9 %edi %esi %edx %ecx %r8d %r9d %di %si %dx %cx %r82 %r9w %dl %sil %dl %cl %r8b %r9b per esempio : void proc ( long long int int short short char char ) ; a1, *a1p, a2, *a2p, a3, *a3p, a4, *a4p // // // // // // // // %rdi %rsi %edx %rcx %r8w %r9 8(%rsp) 16(%rsp) Stack frame Abbiamo già visto che molte funzioni non richiedono l'utilizzo di un stack frame (%ebp) se tutte le variabili locali possono essere mantenute nel registro, l'unica funzione del registro %ebp è quella di mantenere un indirizzo di ritorno. Le ragioni per cui una funzione utilizza uno stack frame sono queste : - ci sono troppe variabili locali per essere mantenute nei registri. - alcune variabili locali sono array di strutture ; - la funzione deve passare argomenti allo stack verso un'altra ; - la funzione deve salvare alcuni registri prima di modificarli quando una di questi situazioni occorre, il compilatori genera la gestione dello stack frame, contrariamente nell'architettura a 32 bit che lo stack frame poteva variare, nell' x86-64 normalmente ha una dimensione fissa. Definita all'inizio della procedura decrementando il registro %rsp. Quindi non ci riferiamo alle variabili locali solo utilizzando %esp. E' per questo motivo che il registro %ebp ora non ci serve più. esempio : long int call_proc() [ long x1=1 int x2=2 ; short x3=3 char x4=4 proc ] ;#8 #4 ;#2 ;#1 byte byte byte byte # 16 byte stack locale (x1,&x1,x2,&x2,x3,&x3,x4,&x4); return (x1+x2)*(x3-x4) ; in assembly : call_proc: subq $32 , %rsp # # # alloca 32 byte stack frame 16 byte per parametri 7° 8° 16 byte per le variabili movl movl $2 $3 , , %edx %rd8 # # 3° argomento 5° argomento leaq leaq leaq leaq movl 31(%rsp), 24(%rsp), 28(%esp), 16(%rsp), $1 , %rax %rcx %r9 %rsi %edi # # # # 4° argomento 6° argomento 2° argomento 1° argomento x1=1 8° argomento movq $1 movq %rax , , 16(%rsp) 8(%rsp) # # movq movq movq movl , , , , 24(%rsp) 28(%rsp) 31(%rsp) (%rsp) # # x2=2 # x3=3 # x4=4 7° argomento $2 $3 $4 $4 call proc movwl 28(%rsp) movbl 31(%rsp) movwl 24(%rsp) ... , , , %edx %ecx %rax In Questo esempio mi interessa solo sottolineare il passaggio dei parametri alle funzioni, al registro generale %rsp (stack pointer) viene sottratto 32 per assegnare spazio alle variabili locali. Gli ultimi due parametri (7°,8°) sono passati rispettivamente come (%rsp) e 8(%rsp) da 16(%rsp) in poi sono memorizzate le variabili locali. Ricorsione Ancora! in questo caso vi mostro una routine e alcune caratteristiche della manipolazione dei registri, e una novità 'inusuale' del x86-64. # argomenti (%rdi,%rsi) fattoriale: movq %rbx , movq %rbp , subq $24 , ... # routine ... leaq -1(%rdi), movq %rsp , call -16(%rsp) -8(%rsp) %rsp # # # salva %rbx salva %rbp alloca 24 byte %rdi %rsi # # 1° argomento 2° argomento %rbx %rbp %rsp # # # restore %rbx restore rbp dealloca memoria locale fattoriale ... #routine ... movq 8(%rsp), movq 16(%rsp), addq $24 , ret Mi interessava in questo paragrafino solo evidenziare il meccanismo di funzionamento della routine , il passaggio dei prametri e lo stack, non ovviamente la routine in se stessa. 1) la routine salva i due registro (callee-saved) (%ebx,%ebp) ; 2) i registri vengono salvati prima che lo stack pointer venisse diminuito ! La capacità di accedere alla memoria al di là dello stack pointer è una caratteristica 'inusuale' dell' x86-64. Il processore richiede, che il sistema di gestione della memoria virtuale, allochi spazio per questa regione. Le specifiche ABI, si riferiscono ad un area di 128 byte al di la' dello stack pointer. Questa viene definita “red zone”. Floating Point Movimento/Conversione Di seguito fornisco un breve elenco riguardante le istruzioni per il trasferimento di dati tra interi e reali. Ricordo che per un corretto trasferimento le variabili a 64 bit devono soddisfare un allineamento corretto a 8-byte. movlpd movq cvttsd2siq xorps movsd movss (%rdx) (%rcx) %xmm0 %xmm0 %xmm0 %xmm0 , , , , , , %xmm0 %xmm1 %rax %xmm0 (%rdx) (%rsi) Benchè ci sia ancora molto da dire sulla gestione dei numeri in virgola mobile e propinare una serie di esempi preferisco, aver solo accennato all'argomento. CAPITOLO 25 Disassembling Disassembling Benvenuti in questo ultimo capitolo del libro. Questa parte è dedicata tutto sul “disassembling” o smontaggio dei programmi senza informazioni di debug. Non è da considerarsi sicuramente un invito al cracking degli stessi, piuttosto come materiale informativo per aver le basi su cui applicare i concetti finora appresi. Questa parte è sicuramente la più difficile del libro in quanto per disassemblare un programma, cioè smomtarlo passo a passo occorre un'ottima conoscenza del sistema operativo, delle librerie collegate e del linguaggio macchina ed in alcuni casi dei trucchi che utilizzano i programmatori per nascondere il codice : “obfuscanting code”. Inizierò con l'utilizzo di alcuni programmi per avere delle informazioni sulll'eseguibile e poi passare al debugger con le informazioni acquisite. N.B. No part of this project may be used to break the law, or to cause damage of any kind. And i'm not responsible for anything you do with it. Nessuna parte di questo progetto può essere usato per violare la legge, o causare danni agli altri di qualsiasi tipo. Non mi ritengo responsabile per qualsiasi cosa facciate con esso. READELF Questo programma visualizza le informazioni relativamente ad un file in formato ELF. .data str: .asciz "\nHello World!\n" .text .globl _start _start: pushl $str call puts addl $4,%esp movl $1,%eax movl $0,%ebx int $0x80 Compiliamo programma. ed linkiamo al fine di otterere l'eseguibile questo piccolo debian:~/prova# readelf -h uno.bin ELF Header: Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Class: ELF32 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: EXEC (Executable file) Machine: Intel 80386 Version: 0x1 Entry point address: 0x8048184 Start of program headers: 52 (bytes into file) Start of section headers: 732 (bytes into file) Flags: 0x0 Size of this header: 52 (bytes) Size of program headers: 32 (bytes) Number of program headers: 5 Size of section headers: 40 (bytes) Number of section headers: 17 Section header string table index: 14 debian:~/prova# Da qui possiamo evincere alcune informazioni, si tratta ovvviamente di un file binario in formato elf32, dai primo byte del binario “ 7f 45 4c 46” , è un file eseguibile compilato su un architettura 386 importante per i nostri scopi è l'indirizzo di partenza 0x8048184, da qui in poi inizieremo a disassemblare il programma. Questo è un piccolo esempio esadecimale da voi preferito : 00000000 7F 00000010 02 00000020 E0 00000030 11 00000040 34 00000050 04 00000060 D4 00000070 01 00000080 00 00000090 00 000000A0 A0 000000B0 00 000000C0 B0 000000D0 04 000000E0 78 000000F0 01 00000100 00 00000110 00 00000120 63 00000130 43 00000140 01 00000150 00 00000160 07 00000170 00 00000180 E0 00000190 04 000001A0 0A 000001B0 01 000001C0 05 000001D0 0A 000001E0 15 000001F0 02 00000200 17 00000210 FF 00000220 00 00000230 00 00000240 00 00000250 B0 --- uno.bin 45 00 02 00 80 00 80 00 80 10 91 10 91 00 2E 00 00 00 2E 5F 00 00 01 00 FF B8 48 00 00 00 00 00 00 FF 00 00 00 91 del programma hexedit, o di qualsiasi editor 4C 46 01 01 01 00 00 00 00 00 00 00 00 00 .ELF............ 03 00 01 00 00 00 84 81 04 08 34 00 00 00 ............4... 00 00 00 00 00 00 34 00 20 00 05 00 28 00 ........4. ...(. 0E 00 06 00 00 00 34 00 00 00 34 80 04 08 ........4...4... 04 08 A0 00 00 00 A0 00 00 00 05 00 00 00 4............... 00 00 03 00 00 00 D4 00 00 00 D4 80 04 08 ................ 04 08 13 00 00 00 13 00 00 00 04 00 00 00 ................ 00 00 01 00 00 00 00 00 00 00 00 80 04 08 ................ 04 08 9D 01 00 00 9D 01 00 00 05 00 00 00 ................ 00 00 01 00 00 00 A0 01 00 00 A0 91 04 08 ................ 04 08 C0 00 00 00 C0 00 00 00 06 00 00 00 ................ 00 00 02 00 00 00 B0 01 00 00 B0 91 04 08 ................ 04 08 A0 00 00 00 A0 00 00 00 06 00 00 00 ................ 00 00 2F 6C 69 62 2F 6C 64 2D 6C 69 6E 75 ..../lib/ld-linu 73 6F 2E 32 00 00 01 00 00 00 02 00 00 00 x.so.2.......... 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00 00 00 00 00 00 00 00 00 00 0B 00 00 00 ................ 00 00 7F 01 00 00 12 00 00 00 00 6C 69 62 .............lib 73 6F 2E 36 00 70 75 74 73 00 47 4C 49 42 c.so.6.puts.GLIB 32 2E 30 00 00 00 02 00 00 00 01 00 01 00 C_2.0........... 00 00 10 00 00 00 00 00 00 00 10 69 69 0D .............ii. 02 00 10 00 00 00 00 00 00 00 5C 92 04 08 ............\... 00 00 FF 35 54 92 04 08 FF 25 58 92 04 08 .....5T....%X... 00 00 FF 25 5C 92 04 08 68 00 00 00 00 E9 .....%\...h..... FF FF 68 A0 91 04 08 E8 E6 FF FF FF 83 C4 ....h........... 01 00 00 00 BB 00 00 00 00 CD 80 00 00 00 ................ 65 6C 6C 6F 20 57 6F 72 6C 64 21 0A 00 00 .Hello World!... 00 00 01 00 00 00 04 00 00 00 E8 80 04 08 ................ 00 00 1C 81 04 08 06 00 00 00 FC 80 04 08 ................ 00 00 1A 00 00 00 0B 00 00 00 10 00 00 00 ................ 00 00 00 00 00 00 03 00 00 00 50 92 04 08 ............P... 00 00 08 00 00 00 14 00 00 00 11 00 00 00 ................ 00 00 5C 81 04 08 FE FF FF 6F 3C 81 04 08 ....\......o<... FF 6F 01 00 00 00 F0 FF FF 6F 36 81 04 08 ...o.......o6... 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 04 08 00 00 00 00 00 00 00 00 7A 81 04 08 ............z... --0x0/0x76B------------------------------------------------- Da qui è possibile vedere il formato elf32, il linker dinamico la libreria caricata, la funzione e la stringa. Eseguendo gdb con le informazioni ricavate precedentemente riusciamo a vedere correttamente cosa fa il programma, dal suo punto di ingresso in poi. (gdb) disass 0x8048184 Dump of assembler code for function _start: 0x08048184 <_start+0>: push $0x80491a0 0x08048189 <_start+5>: call 0x8048174 0x0804818e <_start+10>: add $0x4,%esp 0x08048191 <_start+13>: mov $0x1,%eax 0x08048196 <_start+18>: mov $0x0,%ebx 0x0804819b <_start+23>: int $0x80 End of assembler dump. (gdb) Al momento posso dire che passa un indirizzo di memoria ad una subroutine e ne termina l'esecuzione con codice di ritorno '0' con la syscall exit. Ora andiamo ad investigare cosa è contenuto nella locazione $0x80491a0, (gdb) x/20c 0x80491a0 0x80491a0 <str>: 10 '\n' 72 'H' 101 'e' 108 'l' 108 'l' 111 'o' 32 ' ' 87 'W' 0x80491a8 <str+8>: 111 'o' 114 'r' 108 'l' 100 'd' 33 '!' 10 '\n' 0 '\0' Cannot access memory at address 0x80491af (gdb) notate il messaggio di errore di gdb, è molto utile in quanto ci dice che non può accedere oltre un determinato indirizzo della zona di memoria da noi richiesta. quindi 0x80491af, è l'ultima locazione della momoria dati allocata accessibile. (gdb) x/15c 0x80491a0 0x80491a0 <str>: 87 'W' 0x80491a8 <str+8>: (gdb) 10 '\n' 72 'H' 101 'e' 108 'l' 108 'l' 111 'o' 32 ' ' 111 'o' 114 'r' 108 'l' 100 'd' 33 '!' 10 '\n' 0 '\0' La differenza tra i due indirizzi è esattamente 15 '0x0F'. In questo caso alla funzione viene passata una stringa contentene “\nHellow World!\n”. Quasi mai arriviamo ad ottenere questo errore, per la fine della stringa fate sempre riferimento al carattere 'NULL'. Ora non sapendo di che funzioni si tratti, propongo un bel break point nella shared library. (gdb) break 0x8048189 Function "0x8048189" not defined. Make breakpoint pending on future shared library load? (y or [n]) y Breakpoint 1 (0x8048189) pending. (gdb) Come potete vedere il linker dinamico chiama la 'shared library' libc e ne indirizza l'esecuzione alla funzione di libreria 'PUTS'. Breakpoint 2, 0x08048184 in _start () (gdb) s Single stepping until exit from function _start, which has no line number information. 0x40088b20 in puts () from /lib/tls/libc.so.6 (gdb) infine con l'istruzione “cont” possiamo terminare il programma. (gdb) cont Continuing. Hello World! Program exited normally. In questo semplice caso abbiamo ottenuto tutte le informazioni che ci servivano per debuggare il programma e vedere cosa effettivmante fa. Non contenti è ora di modificare l'eseguibile e fargli fare ciò che volgliamo. Digitate questa breave linea e annotatevi il parametro di uscita '0' appunto quello impostato in %ebx. debian:~/prova# ./uno.bin ; echo $? Hello World! 0 debian:~/prova# OBJDUMP / HEXEDIT Ora è la volta di un altro programma 'hexedit' che è un editor esadecimale col quale possiamo visualizzare gli opcode in formato macchina. Utilizzate un' altro comodo comando objdump che consente di visualizzare il formato delle istruzioni con i relativi opcode in linguaggio macchina dal punto di partenza debian:~/prova# objdump -d uno.bin uno.bin: file format elf32-i386 Disassembly of section .plt: 08048164 <.plt>: 8048164: ff 35 54 92 04 08 804816a: ff 25 58 92 04 08 8048170: 00 00 8048172: 00 00 8048174: ff 25 5c 92 04 08 804817a: 68 00 00 00 00 804817f: e9 e0 ff ff ff Disassembly of section .text: pushl jmp add add jmp push jmp 0x8049254 *0x8049258 %al,(%eax) %al,(%eax) *0x804925c $0x0 8048164 <_start-0x20> 08048184 <_start>: 8048184: 68 8048189: e8 804818e: 83 8048191: b8 8048196: bb 804819b: cd debian:~/prova# push call add mov mov int $0x80491a0 8048174 <_start-0x10> $0x4,%esp $0x1,%eax $0x0,%ebx $0x80 a0 e6 c4 01 00 80 91 ff 04 00 00 04 08 ff ff 00 00 00 00 A noi interessa la sezione “.text”, ricordate la costruzione degli opcode ? L'istruzione movl $0x0,%ebx viene codificato come bb 00 00 00 00 ovviamente con la notazione intel little endian. Ora supponiamo di voler far ritornare al programma un errore dovrò mettere nel registro %ebx un altro valore per esempio 1. Quindi dovrò trasformare la sequenza di codici da bb 00 00 00 00 a bb 01 00 00 00. Ricordate 4 byte perchè si tratta di un long e la notazione inversa di intel. Detto fatto , mano al programma hexedit e con control+f ricerchiama questa sequenza poi f2 e salviamo il tutto. 00000110 00 00 00 00 7F 01 00 00 12 00 00 00 00 6C 69 62 .............lib Hexa string to search: () bb00000000 00000150 00 00 02 00 10 00 00 00 00 00 00 00 5C 92 04 08 ............\... ora il cursore si ferma proprio sopra l'indirizzo contenente il byte da noi desiderato, utilizzate sempre (ovvio) delle sequenze lunghe per essere sicuri che si tratti dell'istruzione che state effettivamente cercando. 00000190 000001A0 000001B0 04 B8 01 00 0A 48 65 6C 01 00 00 00 00 00 BB 00 6C 6F 20 57 01 00 00 00 00 00 00 CD 6F 72 6C 64 04 00 00 00 80 00 00 00 21 0A 00 00 E8 80 04 08 ................ .Hello World!... ................ Questo è il codice dove si è posizionato il cursore, sicuramente si tratta della nostra istruzione cui facciamo riferimento in quanto la succesiva è cd 80 appunto (int $0x80). 00000190 000001A0 000001B0 04 B8 01 00 0A 48 65 6C 01 00 00 00 00 00 BB 01 6C 6F 20 57 01 00 00 00 00 00 00 CD 6F 72 6C 64 04 00 00 00 80 00 00 00 21 0A 00 00 E8 80 04 08 ................ .Hello World!... ................ ora cambiamo questo opcode con 01 salvate con f2 e digitate questa linea : debian:~/prova# ./uno.bin ; echo $? Hello World! 1 debian:~/prova# come potete vedere stavolta è cambiato l'output in quanto il programma è stato modificato. N.B. No part of this project may be used to break the law, or to cause damage of any kind. And i'm not responsible for anything you do with it. Nessuna parte di questo progetto può essere usato per violare la legge, o causare danni agli altri di qualsiasi tipo. Non mi ritengo responsabile per qualsiasi cosa facciate con esso. Claudio daffra Scrivere codice in Run Time Digitate questo esempio : .data xxx1: .byte .byte .byte .byte .byte 0xff 0x01 0x00 0x00 0x00 # movl $01,%eax ; (ff illegal opcode) xxx2: .byte .byte .byte .byte .byte 0xff 0x00 0x00 0x00 0x00 # movl $00,$ebx ; (ff illegal opcode) .byte 0xcd .byte 0x80 # int $0x80 .text _start: .globl xxx1 .globl xxx2 .globl _start movb $0xb8 , (xxx1) movb $0xbb , (xxx2) jmp xxx1 Cosa fa è chiaro se abbiamo in mente l'esempio precedente, tuttavia il programma si modifica in run time, quindi per poterlo disassamblare dovremo debuggarlo in esecuzione altrimenti i codici verrano fuorviati. questo è objdump con la sezione codice, poi prosegue alla sezione data 08048100 <_start>: 8048100: c6 05 14 91 04 08 b8 8048107: c6 05 19 91 04 08 bb 804810e: e9 01 10 00 00 debian:~/prova# movb movb jmp $0xb8,0x8049114 $0xbb,0x8049119 8049114 <xxx1> questo è l'esempio se non tentiamo di disassemblare la zona data non ancora modficata. 0x08049114 <xxx1+0>: 0x08049116 <xxx1+2>: 0x08049118 <xxx1+4>: incl add add (%ecx) %al,(%eax) %bh,%bh questa la sezione disassemblata dopo la prima esecuzione del codice 0x08049114 <xxx1+0>: mov $0x1,%eax Smontaggio Codice Altro esempio di smontaggio codice. Non partirò dal codice in C, ma direttamente dall'listato e dall' object dump : 080483e4 <main>: 80483e4: 8d 80483e8: 83 80483eb: ff 80483ee: 55 80483ef: 89 80483f1: 57 80483f2: 56 80483f3: 51 80483f4: 81 80483fa: c7 8048401: e8 8048406: 8d 804840c: 89 8048410: c7 8048417: e8 804841c: c6 8048420: 8d 8048426: 89 804842c: c7 8048433: 85 8048436: c7 804843d: 00 8048440: fc 8048441: 8b 8048447: 8b 804844d: 8b 8048453: f3 8048455: 0f 8048458: 0f 804845b: 89 804845d: 28 804845f: 89 8048461: 0f 8048464: 85 8048466: 74 8048468: c7 804846f: e8 8048474: c7 804847b: e8 8048480: c7 8048487: e8 804848c: b8 8048491: 81 8048497: 59 8048498: 5e 8048499: 5f 804849a: 5d 804849b: 8d 804849e: c3 804849f: 90 4c 24 04 e4 f0 71 fc e5 ec 04 ea 85 44 04 e4 45 85 85 85 04 85 00 9c 24 fe 74 24 24 fe c4 74 70 6c 08 68 00 00 50 ff ff 04 58 ff 00 ff ff ff 00 00 85 04 08 ff ff ff b5 bd 8d a6 97 92 d1 c1 c8 be c0 18 04 7c 04 a0 04 64 00 c4 70 ff ff ff 6c ff ff ff 68 ff ff ff 85 04 08 ff ff ff ff ff ff ff 5e ff ff ff 07 c2 c0 c0 24 fe 24 fe 24 fe 00 9c 61 fc 65 ff 01 ff 7f ff 00 00 85 ff 00 ff 85 ff 00 00 04 08 00 00 04 08 00 lea and pushl push mov push push push sub movl call lea mov movl call movb lea mov movl 0x4(%esp),%ecx $0xfffffff0,%esp 0xfffffffc(%ecx) %ebp %esp,%ebp %edi %esi %ecx $0x9c,%esp $0x8048550,(%esp) 80482f0 <puts@plt> 0xffffff74(%ebp),%eax %eax,0x4(%esp) $0x8048558,(%esp) 8048300 <scanf@plt> $0x0,0xffffffc4(%ebp) 0xffffff74(%ebp),%eax %eax,0xffffff70(%ebp) $0x804855e,0xffffff6c(%ebp) movl $0x7,0xffffff68(%ebp) cld mov 0xffffff70(%ebp),%esi mov 0xffffff6c(%ebp),%edi mov 0xffffff68(%ebp),%ecx repz cmpsb %es:(%edi),%ds:(%esi) seta %dl setb %al mov %edx,%ecx sub %al,%cl mov %ecx,%eax movsbl %al,%eax test %eax,%eax je 8048480 <main+0x9c> movl $0x8048565,(%esp) call 80482f0 <puts@plt> movl $0x1,(%esp) call 8048320 <exit@plt> movl $0x804857f,(%esp) call 80482f0 <puts@plt> mov $0x0,%eax add $0x9c,%esp pop %ecx pop %esi pop %edi pop %ebp lea 0xfffffffc(%ecx),%esp ret nop Il listato è programmato in C, e fa uso delle librerie : string.h, stdlib.h,stdio.h, come puoi vedere da quste funzioni : call 8048320 <exit@plt> ,call 80482f0 <puts@plt>, call 8048300 <scanf@plt>, è anche presente come macro, la strcmp come appare in questo esempio : 8048440: 8048441: 8048447: 804844d: 8048453: 8048455: 8048458: 804845b: 804845d: 804845f: 8048461: 8048464: fc 8b 8b 8b f3 0f 0f 89 28 89 0f 85 b5 bd 8d a6 97 92 d1 c1 c8 be c0 cld mov 0xffffff70(%ebp),%esi mov 0xffffff6c(%ebp),%edi mov 0xffffff68(%ebp),%ecx repz cmpsb %es:(%edi),%ds:(%esi) seta %dl setb %al mov %edx,%ecx sub %al,%cl mov %ecx,%eax movsbl %al,%eax test %eax,%eax 70 ff ff ff 6c ff ff ff 68 ff ff ff c2 c0 c0 Questo pezzo di codice ha come scopo il confronto, di due stringe, destinazione (%edi) e sorgente (%esi), quando una delle due presenta un carattere diverso esce dal ciclo, viene testato il parametro di ritorno e se questa è zero cioè uguale o meglio se le stringhe coincidono allora continua il programma a : 8048466: 74 18 je 8048480 <main+0x9c Ora noi vogliamo che indipendentemente dal risultato salti comunque a questa locazione ( nel nostro caso si trattava del confronto di una password ) ; (gdb) x/8c 0x8048550 0x8048550 <_IO_stdin_used+4>: 112 'p' 97 'a' 115 's' 115 's' 32 ' ' (gdb) 58 ':' 32 ' ' 0 '\0' Il nostro obbiettivo è di sostituire la 'je' con l'opcode 'jmp', attenzione in quanto gli opcode a seconda dell'indirizzamente, del registro assumento valori diversi per esmpio : jmp jmp jmp jmp jmp jmp disp8 disp16/32 mem 16/32/64 mreg 16/32/64 mem16:16/32 ptr16:16/32 0xEB 0xE9 0xFF 0xFF 0xFF 0xEA (short) (near) (indirect) (indirect) (far,indirect) (far,indirect) nel nostro caso occorrerà sostiturlo con un 0xEB, nel mio caso mi avvalgo di khexedit e ricerco la stringa di caratteri : 8048464: 8048466: 0000:04a0 85 c0 74 18 c8 0f be c0 85 c0 test je %eax,%eax 8048480 <main+0x9c> 74 18 c7 04 24 65 85 04 08 e8 ora occerre sostiture 74 con 0xEB (jmp short), come al solito invito a ricercare delle sequenze di 8 byte circa proprio per evitare di modifcare altre parti di codice non interessate. Prima : pass : ciao ?! Wrong User Password [claudio@fedora5]$ Dopo la modifica pass : ciao Welcome ... [claudio@fedora5]$ Al momento è tutto, spero di esservi stato in qualche modo d'aiuto ! cordiali saluti Claudio Daffra