This is an old revision of the document!


Laborator 11 - Considerente hardware și mașini virtuale

Suport laborator

Resurse

Demo

O bună cunoaștere a hardware-ului folosit duce la implementarea unui software mai bun și asigură că putem folosi la maximum resursele de care dispunem.

1. Investigare hardware

In Linux există mai multe comenzi care ajută la descoperirea informațiilor legate de hardware-ul folosit, vizualizarea proprietăților acestora, dar și a diferitelor atribute ale componentelor.

O comandă generalistă, care listează toate informațiile despre sistem este lshw. Rulată fără niciun argument, aceasta va afișa un raport al tuturor componentelor detectate în sistem. Pentru a rezuma însă informațiile, se poate folosi argumentul -short:

$ lshw -short

Pentru a avea acces la și mai multe informații, rulați comanda folosind sudo.

Aceeași comandă ne pune la dispoziție și argumente pentru afișarea informațiilor specifice unei anumite componente (cpu, memory, disk, network, etc.):

$ lshw -class cpu
  *-cpu                   
       product: Intel(R) Core(TM) i5-3470 CPU @ 3.20GHz
       vendor: Intel Corp.
       physical id: 1
       bus info: cpu@0
       size: 1600MHz
       capacity: 1600MHz
       width: 64 bits
[...]
$ lshw -class memory
  *-memory                
       description: System memory
       physical id: 0
       size: 3828MiB
[...]

Există, în schimb, și o serie de comenzi specializate, care realizează inspectarea anumitor componente, oferind în același timp, mai multe informații:

  • lscpu: informații despre microprocesor
$ lscpu
Architecture:          x86_64
CPU op-mode(s):        32-bit, 64-bit
Byte Order:            Little Endian
CPU(s):                4
On-line CPU(s) list:   0-3
Thread(s) per core:    1
Core(s) per socket:    4
[...]
  • free: starea memoriei RAM și swap
$ free
             total       used       free     shared    buffers     cached
Mem:       3920404    2370736    1549668     149668     152316    1102860
-/+ buffers/cache:    1115560    2804844
Swap:      8000508          0    8000508
  • lsblk: informații despre disk-uri și partiții
$ lsblk
NAME   MAJ:MIN RM   SIZE RO TYPE MOUNTPOINT
sda      8:0    0 465,8G  0 disk 
├─sda1   8:1    0 190,8G  0 part /
├─sda2   8:2    0 190,8G  0 part /.systmp
├─sda3   8:3    0   7,6G  0 part [SWAP]
└─sda4   8:4    0    10G  0 part

Aceste comenzi sunt doar niște wrappers peste fișierele corespunzătoare din /proc.

$ ls -l /proc
[...]
-r--r--r--  1 root  root   0 dec  1 11:41 cpuinfo
-r--r--r--  1 root  root   0 dec  1 11:41 meminfo
-r--r--r--  1 root  root   0 dec  1 11:41 partitions
-r--r--r--  1 root  root   0 dec  1 10:51 swaps
[...]

Încă o comandă importantă este uname care listează informații despre sistemul de operare și sistemul fizic. Parametrul -a listează toate informațiile dar acestea pot fi filtrate folosind alți parametrii.

$ uname -a
Linux virtual 3.13.0-32-generic #57-Ubuntu SMP Tue Jul 15 03:51:08 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux

2. Comanda dd

Comanda dd este des folosită în lucrul cu fișiere și dispozitive. Aceasta realizează transferul în blocuri de la sursă la destinație. Poate fi folosită pentru construirea unor fișiere cu dimensiune fixă, shredding-ul informațiilor dintr-o anumita zone de memorie (prin scriere cu zero), recuperarea datelor de pe un disk corupt, backup-ul dispozitivelor etc.

Următorul apel al comenzii va umple fisierul myfile.dat cu 102kB (1024 * 100 = 102400) de informație, conținând doar zero-uri.

$ dd if=/dev/zero of=myfile.dat count=100 bs=1K

if (input file) și of (output file) determină fișierele sursă/destinație ale comenzii. bs (block size) și count determină dimensiunea informațiilor care vor fi copiate. Un bs mare poate asigura o copiere mai rapidă, în timp ce unul mai scazut, asigură integritatea datelor în cazul unui recuperari a datelor de pe un disk corupt.

dd are o mulțime de întrebuințări, printre care cele mai uzuale sunt: scrierea unei imagini (.iso) pe un dispozitiv (stick usb):

$ dd if=~/my_image.iso of=/dev/sdb bs=4M

sau pentru a crea o imagine a unui CD-ROM, pentru backup sau transfer:

$ dd if=/dev/sr0 of=mycd.iso bs=8M

Alte întrebuințări interesante ale lui dd ar fi următoarele:

Generarea unui fișier de 32MB cu date aleatoare:

$ dd if=/dev/urandom of=~/myfile.bin bs=4M count=8

Generarea unui fișier de 32MB cu octeți de 0:

$ dd if=/dev/zero of=~/myfile.bin bs=4M count=8

Formatarea unei partiții: prima comandă șterge tabela de partiții a unui stick USB aflat în /dev/sdc, a doua comandă formatează stickul USB aflat în /dev/sdc. Dacă doriți să formatați un stick trebuie să vă asigurați că acesta a fost demontat în prealabil (folosiți umount /dev/sdc1):

$ dd if=/dev/zero of=/dev/sdc bs=512 count=16
$ dd if=/dev/zero of=/dev/sdc bs=1M

Despre dd (destoryer of disks) puteți citi mai multe în acest articol

3. Secvența de boot

Pornirea unui sistem de calcul vine cu o problemă interesantă: PC-ul trebuie sa execute cod, înainte ca acesta să fie în memorie. Atunci avem nevoie de o secvență mai amplă, ce trebuie sa parcurgă mai multe stadii, până să ajungă să ruleze efectiv sistemul de operare. Această secvență implică inițializarea componentelor si testarea lor minimală, selectarea între mai multe dispozitive boot-abile, selectarea între mai multe sisteme de operare, încărcarea imaginii de kernel și pornirea programelor inițiale, totul într-un timp cât mai scurt.

Un model comun pentru toate arhitecturile, al secvenței de boot, este următorul:

  1. BIOS (Basic Input Output System) - inițializează și verifică hardware-ul; în prezent se încearcă înlocuirea sa cu UEFI
  2. Boot sector (Master Boot Record) - reprezentat de primii octeți ai unui HDD, CD, stick USB
  3. Boot loader (second-stage boot loader) - permite selectarea între mai multe sisteme de operare, imagini de kernel, dar și modificarea parametrilor de boot; execmple de bootloadere: GRUB2, LILO, Windows Boot Manager
  4. Încărcare kernel și drivere
  5. Pornire init: procesul părinte al tuturor
  6. Pornire daemoni din scripturile de iniţializare
  7. Pornire programe de login şi aşteptare autentificare utilizator

Directorul /boot conține imaginea de kernel (sau mai multe, pentru diferite versiuni) care este încărcată la pornirea sistemului, dar și fișiere de configurare pentru bootloader.

$ ls -l /boot
[...]
drwxr-xr-x 3 root root    4096 Nov  7 00:00 grub
[...]
-rw-r--r-- 1 root root 4151832 Jun 11  2013 vmlinuz-2.6.32-49
-rw-r--r-- 1 root root 4124072 Jul 11  2013 vmlinuz-2.6.32-50
-rw-r--r-- 1 root root 4151072 Sep 11  2013 vmlinuz-2.6.32-51
[...]

4. Virtualizare

Virtualizarea este o tehnologie care partajează și alocă resursele hardware ale unui server în mai multe “mașini virtuale” (VM = Virtual Machine) și creează posibilitatea rulării simultane a mai multor sisteme de operare pe un singur computer.

Cel mai important avantaj pe care il aduce virtualizarea, apare atunci când o aplicație încetează să mai funcționeze, însă aceasta nu va antrena și căderea celorlalte, deoarece aplicațiile rulează pe mașini virtuale independente, separate, chiar dacă ele se află pe același spațiu fizic. În plus, chiar dacă serverul hardware se prăbușește, virtualizarea are abilitatea de a migra în timp real toate informațiile pe un alt server, fără să afecteze funcționalitatea programelor.

Virtualizarea stă la baza arhitecturilor de tip “cloud”, însă nu se oprește doar acolo. Așa cum o să vedem și în laborator, rularea unui sistem de operare în cadrul altuia, poate avea numeroase beneficii.

Pentru gestiunea mașinilor virtuale dintr-un sistem, VirtualBox ne pune la dispoziți suita de comenzi vboxmanage. Aceasta poate să:

  • listeze toate mașinile virtuale
 $ vboxmanage list vms 
  • sau doar a celoar care sunt pornite în mod curent
 $ vboxmanage list runningvms 
  • oprirea unei mașini după nume
 $ vboxmanage controlvm NUME_VM acpipowerbutton 
  • restart-area mașinii
 $ vboxmanage controlvm NUME_VM reset 
  • pornirea acesteia, folosind numele
 $ vboxmanage startvm NUME_VM 

O funcționalitate utilă a unei mașini virtuale este aceea de a-și salva starea curentă, prin trecerea în starea de pauză (pause), în care nu consumă resurse, și revenirea la starea inițială atunci cand e nevoie de ea (resume). Comnezile folosite sunt:

  • trecerea în starea de pauză
 $ vboxmanage controlvm NUME_VM pause 
  • revenirea la starea de rulare
 $ vboxmanage controlvm NUME_VM resume 

Aceleași funcționalități pot fi obținute și folosind interfața grafică cu care vine VirtualBox

5. Emulare

În dezvoltarea sistemelor embedded e folosita deoarece poate emula un sistem de calcul complet, nefiind necesar ca sistemul țintă (target) pentru care se face dezvoltarea, și sistemul host, pe care se face dezvoltarea, să folosească aceeași arhitectură. Acest lucru permite ca dezvoltarea software-ului pentru un sistem embedded să poată fi făcută în paralel cu proiectarea hardware-ului, lucru crucial pentru obținerea unui timp de dezvoltare scurt. Un alt avantaj pe care il poate avea emularea, mai ales a sistemelor low-end, este o viteză superioară a emulării pe un sistem host performant, în comparație cu sistemul target.

Un exemplu de emulator este DOSbox folosit pentru a rula jocuri vechi.

QEMU este un emulator/mașină virtuală care permite rularea unui sistem de operare complet ca un simplu program în cadrul unui alt sistem. A fost dezvoltat inițial de Fabrice Bellard și este disponibil gratuit, sub o licență open source. QEMU poate rula atât pe Linux, cât și pe Windows. Este un emulator deoarece poate rula sisteme de operare și programe compilate pentru o platformă (ex: o placă cu procesor ARM, similară cu ce găsim într-un smartphone) pe o altă platformă (ex: un PC arhitectură x86 așa cum sunt sistemele din EG306 sau EG106). Acest lucru este făcut prin translatarea dinamică a intrucțiunilor architecturii guest în instrucțiuni pentru arhitectura host.

Exerciții

1. Pregătire setup și gestiunea mașinilor virtuale (3p)

1.1. Pregătire infrastructură

Deschideți un teminal nou folosind combinația de taste Alt+Ctrl+t. Montati sistemul de fișiere unfrozen folosind comanda:

student@uso:~$ sudo mount /dev/sda7 /mnt/unfrozen

Pentru rezolvarea exercițiilor aveți nevoie de mașina virtuală puccini, aflată pe calculatoarele din EG306 și EG106 pe partiția (/mnt/unfrozen/uso/mv). Mașina se poate descărca din repository-ul facultății, în cazul în care nu o găsiți pe stații. Credențialele de autentificare sunt: utilizator student, parola student.

Verificați existența lor pe mașina de lucru în /mnt/unfrozen/uso/mv. Dacă nu există, chemați asistentul sau descărcați-o chiar voi pe parcursul demo-ului.

De rezolvarea acestui exercițiu depind toate celelalte. Chemați asistentul de fiecare dată când întâmpinați probleme majore

Deschideți VirtualBox și importați fișierul puccini-lab7.ova.

Atenție: când importați mașina în VirtualBox, să bifați opțiunea Reinitialize the MAC address of all network cards!

Avem nevoie de o interfață host-only. E posibil ca atunci când importâm mașina virtuală să primim eroarea Invalid settings detected. Soluția este că trebuie să adăugăm o interfață virtuală de rețea care să conecteze mașina fizică de cea virtuală. Aceasta se face astfel:

Din VirtualBox mergem la File –> Preferences –> Network. Mergem la tab-ul Host-only Netwokrs și apăsăm butonul de add. Urmăriți detalii în poza de mai jos:

Observați că s-a adăugat o nouă interfață vboxnet0.

Conectați-vă prin SSH de pe mașina fizică, pe mașina virtuală și rulați comenzile de acolo, pentru a beneficia de folosirea mouse-lui în terminal, copy-and-paste etc.

Pentru conectarea prin SSH, folosiți adresa IP de pe interfața eth1 a mașinii virtuale pentru conectare.

Va trebui să porniți serviciul de SSH pe mașina virtuală folosind comanda

sudo service ssh start

Înainte de a începe rezolvarea acestui exercitiu, rulați în mașina virtuală următoarea comandă:

student@puccini:~$ echo 127.0.0.1 puccini | sudo tee -a /etc/hosts

1.2. Gestiune mașini virtuale

Am văzut deja utilitatea mașinilor virtuale, fie ca izolare a mediului în care lucrăm, fie ca mediu de testare a unor programe/scripturi/comenzi, pe diferite sisteme de operare.

Dorim să alocăm mai multă memorie și mai mult CPU actualei mașini virtuale. Asta va duce la îmbunătățirea performanțelor acesteia.

Pentru ca putea face astfel de modificări, mașina virtuală trebuie să fie oprită în totalitate (Power Off)!

De pe mașina fizică, folosiți comenzi vboxmanage pentru a lista mașinile virtuale care rulează și opriți mașina pe care lucrăm.

În primul rând aflați numele mașinii virtuale care rulează, folosind pe sistemul fizic comanda

$ vboxmanage list runningvms

Apoi, pentru oprirea mașinii virtuale, folosiți pe sistemul fizic comanda

$ vboxmanage controlvm <NUME_VM> acpipowerbutton

unde <NUME_VM> este numele mașinii virtuale afișat de comanda precedentă.

Din interfața grafică, deschideți meniul Settings al mașinii virtuale și, în tabul System, alocați:

  • 750MB de memorie RAM
  • 2 core-uri de CPU spre utilizare

Salvați modificările și reporniți mașina.

Un feature interesant al VirtualBox realizabil în urma instalării VirtualBox Additions este posibilitatea de a partaja clipboard-ul între host și guest (copy de pe host și paste în guest). Detalii + setări puteți afla de aici.

2. Editarea intrărilor din bootloader (2p)

Înainte de a începe rezolvarea acestui exercitiu, rulați în mașina virtuală următoarea comandă:

student@puccini:~$ echo 127.0.0.1 puccini | sudo tee -a /etc/hosts

Într-un sistem dual-boot sau atunci cand avem un meniu de GRUB prea aglomerat (cu diferite versiuni de kernel) e important să înțelegem modul în care putem personaliza intrările din meniu și cum putem altera setările acestuia (ordinea intrărilor din meniu, valoarea de timeout, optiunea implicită etc.).

Configurați setările de GRUB, pe mașina virtuală, pentru a aștepta (timeout) 7 secunde înainte de a selecta intarea implicită.

Va trebui să modificați fișierul /etc/default/grub. Modificați parametrul corespunzător din fișierul șablon (e vorba de linia ce conține TIMEOUT fără a conține HIDDEN).

Comentați liniile ce conțin parametrii HIDDEN care ascund ecranul de bootloader. Prin comentarea acestor linii vom activa ecranul de bootloader.

Parametrul care este inițializat la valoarea "splash quiet" modificați-l la valoarea "verbose" pentru ca la bootarea să fie afișate cât mai multe mesaje.

Anuntați bootloader-ul să regenereze setările sale folosind comanda

sudo update-grub

Apoi reporniți mașina virtuală folosind comanda

sudo reboot

pentru a verifica funcționarea configurării. Când bootează ecranul de bootloader (GRUB) va aștepta acum 7 secunde înainte de a boota în opțiunea implicită.

Înainte de a reporni mașina virtuală, consultați-vă cu asistentul dacă modificările făcute sunt corecte.

3. Recuperarea parolei de root (2p)

Exista situatii în care sistemul poate deveni inaccesibil: am modificat greșit fișierul /etc/sudoers și nu mai putem folosi sudo, am uitat parola utilizatorului și nu mai putem face login sau dorim setarea unei parole pentru root, dar nu avem drepturi privilegiate.

Este nevoie să găsim o altă cale pentru a accesa sistemul. Este nevoie de recuperarea parolei utilizatorului root.

Pentru a realiza acest lucru trebuie sa configuram sistemul ca atunci cand inițializează kernelul, în loc să pornească procesul init, să deschidă un shell. Și pentru ca utilizatorul root e cel care deține procesele inițiale, și shell-ul nostru o sa porneasca cu drepturi "speciale".

Atenție, din motive de securitate, niciodată nu o să puteți afla parola unui utilizator. În cel mai bun caz o puteți doar suprascrie.

Pe mașina virtuală, reporniți sistemul. În meniul GRUB, apăsați tasta e atunci când intrarea corespunzătoare este selectată. Se va deschide un mic script într-un editor. Printre instrucțiunile de acolo, avem și parametrii cu care pornește kernel-ul (linia care începe cu linux). Adăugați la finalul acelei linii șirul init=/bin/bash, similar liniei de mai jos:

[...]
linux /boot/vmlinuz-3.13.0-39-generic [...] init=/bin/bash
[...]

Apăsați combinația de taste Ctrl+x pentru a salva modificările și a porni sistemul.

În acest moment aveți acces privilegiat la sistem (vedeți prompt-ul de root care se încheie în #) și puteți face orice acțiuni. În mod obișnuit așa se resetează parola de root (folosind comanda passwd) și apoi se bootează în Linux obișnuit și se folosește acea parolă.

Este posibil să nu puteți modifica parola pentru că sistemul de fișiere a fost montat read-only. La rularea comenzii passwd veți primi mesajul de eroare:

passwd: Authentication token manipulation error

În această situație va trebui să remontați fișierul în mod read-write folosind comanda:

mount -o remount /

După ce ați schimbat parola, nu puteți folosi comanda reboot pentru a reporni mașina virtuală. Folosiți interfața grafică VirtualBox pentru repornirea mașinii virtuale. După ce bootează, verificați că parola utilizatorului root este cea proaspăt introdusă.

4. Rularea unei distribuții pentru o altă arhitectură (1p)

Acest exercițiu se desfășoară pe sistemul fizic. Puteți închide mașina virtuală VirtualBox puccini.

Pentru acest exercițiu sunt necesare:

  • O imagine de kernel Linux pentru arhitectura ARM, descărcabil de aici.
  • O imagine de Raspbian (mașină virtuală de Debian care merge pe un sistem de fișiere tip Raspberry Pi), descărcabilă de aici. Folosiți Raspbian Wheezy, un flavor de Ubuntu pentru platforma Raspberry Pi. Dezarhivați imaginea în directorul în care ați descărcat-o folosind comanda
    unzip 2012-10-28-wheezy-raspbian.zip
  • Un set de pachete pentru a rula într-o mașina virtuală QEMU imaginea de Raspbian. Pentru a instala pachetele necesare rulați comanda
    student@uso~:$ sudo apt-get install qemu qemu-kvm qemu-system-arm

Există posibilitatea ca în urma rulării comenzii de mai sus să obțineți mesajele de eroare. Mesajul de eroare vă indică ce aveți de făcut, anume actualizarea pachetelor folosind comanda

sudo apt-get update

după care să rulați din nou comanda de instalare, care va rula acum fără probleme.

Pentru a rula o distribuție de Linux pentru platforma Raspberry Pi, trebuie rulată următoarea comandă în directorul în care aveți atât imaginea de kernel kernel-qemu cât și imaginea de Raspbian.

student@uso~:$ qemu-system-arm -kernel kernel-qemu -cpu arm1176 -m 256 -M versatilepb -no-reboot -serial stdio -append "root=/dev/sda2 panic=1 rootfstype=ext4 rw" -hda 2012-10-28-wheezy-raspbian.img

Să descifrăm fiecare parametru de mai sus:

  • -cpu - specifică tipul de procesor care va fi emulat
  • -m - specifică dimensiunea memoriei RAM
  • -hda, -hdb etc. - specifică imaginea pentru primul hard disk, respectiv al doilea hard disk, ș.a.m.d
  • -fda, -fdb - specifică imaginea pentru primul floppy disk, respectiv al doilea floppy disk
  • -cdrom - specifică imaginea folosită de cdrom
  • -serial, -parallel - specifică porturile seriale, respectiv, paralele și modul de interacțiune a acestora cu host-ul

După bootare vă apare un ecran de configurare. În ecranul apărut mergeți până la butonul Finish (folosind Tab).

Dacă nu sunteți autentificați, folosiți următoarele date de autentificare:

  • username: pi
  • parolă: raspberry

După ce a bootat sistemul de operare din emulatorul QEMU (adică mașina virtuală Raspabian) rulați, în cadrul acestei mașini virtuale, comenzile:

$ lscpu
$ uname -a

Comanda lscpu s-ar putea să nu vă meargă pentru că nu este montat sistemul de fișiere proc necesar comenzii lscpu. Pentru a monta acest sistem de fișiere rulați comanda

$ sudo mount -t proc proc /proc

Observați că în loc de arhitectura x86 în output (așa cum vedeam la primul exercițiu) avem ca arhitectură arm. Mai multe detalii despre ARM pe site-ul oficial.

Informativ: Pentru a scrie imaginea de mai sus pe un card SD și a o rula pe un dispozitiv de tipul Raspberry Pi trebuie să rulăm trei comenzi: una pentru idenficarea partițiilor (e.g. df), una pentru demontarea partiției care repzintă cardul SD și una pentru scrierea imaginii de Raspbian pe cardul SD, folosind dd. Urmăriți indicațiile de aici dacă nu sunteți siguri ce comenzi trebuie să dați.

5. Informații despre sistem (2p)

Pe mașina fizică, folosind comenzile corespunzătoare, determinați următoarele informații despre sistem. Puteți folosi oricare dintre comenzile pe care le cunoașteți:

  • numele stației de lucru (/etc/hostname)
  • arhitectura procesorului (x86, x86_64, aarch64, armv7 etc.) și numărul de core-uri (Indicație: /proc/cpuinfo sau lscpu și nproc).
  • versiunea kernelului de Linux (uname)
  • dimensiunea totală a memoriei RAM, memoria folosită și memoria liberă
  • numărul de partiții din sistem
  • modelul hard disk-ului, device-ul (Indicație: puteți folosi/prelucra output-ul comenzilor lsblk, lshw, hwinfo, inxi
  • cât spațiu mai este disponibil pe hard disk

Agregați informațiile/comenzile obținute mai sus într-un script care să aibă un output similar cu formatul de mai jos (nu trebuie să fie strict ca în exemplul de mai jos):

hostname: ...
architecture type: ...
kernel version: ...
total memory: ...
used memory: ...
free memory: ...
number of partitions: ...
hard disk model: ...
hard disk device: ...
root partition free space: ...

Pentru un bonus de 1 karma WoUSO îmbunătățiți script-ul astfel încât să conțină următoarele informații:

NIC (Network Interface Card) model: ...
graphics card model: ...
monitor model: ...

BONUS

Rularea unor comenzi "periculoase" (1 karma WoUSO)

Exercițiul se desfășoară pe mașina virtuală. Dacă din diverse motive este stricată definitiv, este indicat să re-importați fișierul OVA în VirtualBox.

NU RULAȚI ACESTE COMENZI PE MAȘINA FIZICĂ.

Uneori este bine să fim atenți ce comenzi executăm în terminal, pentru că putem compromite definitiv sistemul.

Ce se întâmplă, de exemplu, dacă rulăm comanda următoare (ca root sau cu sudo):

dd if=/dev/urandom of=/dev/sda bs=512 count=1

Sau comanda

cat /dev/urandom | head -c 512 > /dev/sda

Un alt mod de a compromite partiția principală este rularea comenzii:

mkfs.ext3 /dev/sda

Homedir-ul se poate compromite rulând comanda următoare:

 mv /home/user/* /dev/null 

Acest articol de pe tecmint prezintă comenzile pe care le-am executat mai sus, plus altele la fel de periculoase, împreună cu explicațiile aferente. Alte exemple care merită urmărite le găsiți pe UbuntuGuide.

uso/laboratoare/laborator-11.1513002862.txt.gz · Last modified: 2017/12/11 16:34 by sergiu.weisz
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0