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 all’uscita 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