Context Switching

juin 10th, 2007 at 02:21 admin

Cette fois ci, le contenu du post est sous licence no-brain, tout ceux ayant un QI de plus de 2 sont invités à aller se brancher devant la TV pendant 24h pour le faire diminuer. Pourquoi une telle licence ? Tout simplement parce que ce qui va suivre risque de transformé votre cerveau en éponge. Je prévient d’emblé, ce qui va suivre est un voyage dans le hal et le kernel à travers le fonctionnement du context switching, alors préparez votre aspirine.


Comme vous le savez un processeur monocore ne peut réaliser qu’une seule chose à la fois, l’impression du multitâche sur l’OS est donc réalisée en effectuant plein de petites actions rapidement. Chaque tâche est appelé un thread, un thread représente un ensemble d’instructions à être exécuté, ainsi l’OS pour être multitâche va sauter de thread en thread et le processeur va effectuer quelques instructions de ceux-ci à chaque fois avant de changer de thread. Ca va là, tout le monde suit encore :], je vous conseil quand même d’avoir le chapitre du Windows Internals sur le thread scheduling ouvert pour être plus à l’aise avec la suite.

Un thread peut ainsi avoir 7 états :

- Initialized : thread en cour d’initialisation.
- Ready : le thread est prêt à être éxécuté.
- Running : le thread est en court d’exécution.
- Waiting : le thread est en attente.
- Terminated : le thread à finit son exécution.
- Transition : le thread est ready mais sa kernel stack doit être inswappée pour être rechargée en mémoire.
- Standby : attend d’être exécuté sur un processeur précis.

Chaque thread dispose aussi d’une priorité (en faite il y en à 2, une statique et une dynamique mais osef), celle-ci permet au thread scheduler de déterminer quel sera le thread suivant à être lancé. De plus avant d’être exécuté chaque thread reçois un quantum, cette valeur comparable à une unité de temps, permet à l’OS de définir combien de temps va tourner un thread. En fait, le BIOS va régulièrement générer une interruption qui va décrémenté le quantum du thread courant. Lorsque le quantum est nul, le noyau change de thread. Chez moi cette interruption intervient toutes les 15.625 ms. Cette valeur peut être retrouvée en utilisant l’API GetSystemTimeAdjustment.

En fait il y plusieurs raisons pour que l’OS décide de changer de thread :
- Quand un thread passe en mode Waiting après par un appel à WaitForSingleObject par exemple.
- Un thread de priorité supérieure devient Ready, on appel cela la preemption.
- Le thread à finit son exécution.
- Le quantum du thread courant est expiré.

L’interruption qui intervient toutes les 15.625 ms est généré par le BIOS et est handler par la routine HalpClockInterrupt qu’on retrouve dans l’IDT à l’indice 0×30 :

!idt -a
[...]
30:	806f3d48 hal!HalpClockInterrupt

Comme nous nous trouvons dans le cadre d’une interruption le processeur va automatiquement pusher sur la kernel stack du thread courant les valeurs suivantes :

error code  <- esp after interrupt
registre EIP
registre  CS
registre  EFLAGS
registre  ESP
segment SS

Toutes ces valeurs sont évidemment celles qui existaient avant l’apparition de l’interruption. Si on désassemble le début de la routine HalpClockInterrupt on peut voir :

kd> u hal!HalpClockInterrupt l 20
hal!HalpClockInterrupt:
806f3d48 push    esp
806f3d49 push    ebp
806f3d4a push    ebx
806f3d4b push    esi
806f3d4c push    edi
806f3d4d sub     esp,54h
806f3d50 mov     ebp,esp
806f3d52 mov     dword ptr [esp+44h],eax
806f3d56 mov     dword ptr [esp+40h],ecx
806f3d5a mov     dword ptr [esp+3Ch],edx
806f3d5e test    dword ptr [esp+70h],20000h
806f3d66 jne     hal!V86_Hci_a (806f3d20)
806f3d68 cmp     word ptr [esp+6Ch],8
806f3d6e je      hal!HalpClockInterrupt+0x4b (806f3d93)
806f3d70 mov     word ptr [esp+50h],fs
806f3d74 mov     word ptr [esp+38h],ds
806f3d78 mov     word ptr [esp+34h],es
806f3d7c mov     word ptr [esp+30h],gs
806f3d80 mov     ebx,30h
806f3d85 mov     eax,23h
806f3d8a mov     fs,bx
806f3d8d mov     ds,ax
806f3d90 mov     es,ax
806f3d93 mov     ebx,dword ptr fs:[0]
806f3d9a mov     dword ptr fs:[0],0FFFFFFFFh
806f3da5 mov     dword ptr [esp+4Ch],ebx
[...]

Ce prologue sert à sauvegarde sur la stack les valeurs des autres registres de notre thread interrompu. Plus loin, cette routine élève l’IRQL à CLOCK2_LEVEL (28=0x1C) en appelant HalBeginSystemInterrupt, c’est un des IRQL les plus élevé sachant que le maximum HIGH_LEVEL vaut 31. A la fin de HalpClockInterrupt on voit un jmp sur la fonction du kernel KeUpdateSystemTime qui à pour rôle de mettre à jour l’horloge système.

Plus loin KeUpdateSystemTime appel KeUpdateRunTime qui se charge d’actualiser le compteur d’interruptions, le user ou kernel time du thread et surtout de décrémenter le quantum !

Si on regarde la stack lorsque qu’on est dans KeUpdateRunTime on a :

kd> kv
ChildEBP RetAddr  Args to Child
f98f8d50 804e32f6 00000000 00000000 00000030 nt!KeUpdateRunTime+0x122 (FPO: [1,1,0])
f98f8d50 77bfc7c0 00000000 00000000 00000030 nt!KeUpdateSystemTime+0x137 (FPO: [0,2] TrapFrame @ f98f8d64)

[INTERRUPT OCCURS !!!]

00e3e07c 77c10017 00e3e094 7ca839e4 00e3e104 msvcrt!_aulldvrm
00e3e0b4 7c9ffd32 00e3e46c 0000005b 7ca839e4 msvcrt!_vsnwprintf+0x30 (FPO: [Non-Fpo])
00e3e0d8 7c9ffd02 00e3e46c 0000005c 7ca839e4 SHELL32!StringVPrintfWorkerW+0x2c (FPO: [Non-Fpo])
[...]

Le trapframe correspond aux valeurs des registres sauvegardés sur la kernel stack du thread courant avant que l’interruption ne débute. On peut dumper le contenu du trapframe avec la commande .trap du KD.

kd> .trap f98f8d64
ErrCode = 00000000
eax=00000010 ebx=00000401 ecx=00000007 edx=00000000 esi=00e3de77 edi=00000000
eip=77bfc7c0 esp=00e3dbf8 ebp=00e3e07c iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
msvcrt!_aulldvrm:
001b:77bfc7c0 push    esi

Ici l’interruption à débarqué avant l’exécution du « push esi » de la fonction msvcrt!_aulldvrm.

Dans la routine KeUpdateSystemTime on peut voir le bout ce bout de code :

[...]
nt!KeUpdateSystemTime+0x124:
804e32e3 mov     eax,dword ptr [nt!KeMaximumIncrement (8055168c)]
804e32e8 add     dword ptr [nt!KiTickOffset (80551694)],eax
804e32ee push    dword ptr [esp]
804e32f1 call    nt!KeUpdateRunTime (804e332a)
804e32f6 cli
804e32f7 call    dword ptr [nt!_imp__HalEndSystemInterrupt (804d7608)]
804e32fd jmp     nt!Kei386EoiHelper (804df05c)
[...]

HalEndSystemInterrupt fait redescendre l’IRQL au niveau ou il se trouvait avant l’interruption et Kei386EoiHelper est la fonction qui va nous faire quitter l’interruption. En effet si on la disass on peut voir :

[...]
nt!Kei386EoiHelper+0x12c:
804df188 add     esp,3Ch
804df18b pop     edx
804df18c pop     ecx
804df18d pop     eax
804df18e lea     esp,[ebp+54h]
804df191 pop     edi
804df192 pop     esi
804df193 pop     ebx
804df194 pop     ebp
804df195 cmp     word ptr [esp+8],80h
804df19c ja      nt!Kei386EoiHelper+0x148 (804df1a4)

nt!Kei386EoiHelper+0x142:
804df19e add     esp,4
804df1a1 iretd

nt!Kei386EoiHelper+0x148:
804df1a4 cmp     word ptr [esp+2],0
804df1aa je      nt!Kei386EoiHelper+0x142 (804df19e)

nt!Kei386EoiHelper+0x150:
804df1ac cmp     word ptr [esp],0
804df1b1 jne     nt!Kei386EoiHelper+0x142 (804df19e)

nt!Kei386EoiHelper+0x157:
804df1b3 shr     dword ptr [esp],10h
804df1b7 mov     word ptr [esp+2],0F8h
804df1be lss     sp,dword ptr [esp]
804df1c3 movzx   esp,sp
804df1c6 iretd

Les pop avant sont la pour remettre les registres avant l’arrivé de l’interruption et iretd effectue le retour. Tout est nickel le thread repart.

Je résume donc un peu, le système balance périodiquement une interruption, la routine qui doit gérer cette interruption fait péter l’IRQL à CLOCK2_LEVEL, se charge de mettre à jour l’horloge système et décrémente le quantum du thread courant (en gros). Le contexte de ce dernier est sauvegardé dans sa kernel stack (même si celui était tournait en userland avant que l’interruption arrive). A la fin de l’interrpution la fonction Kei386EoiHelper va reloader le contexte, PAF ! le thread repart et ca fait des chocapics :]

Maintenant on s’intéresse au moment ou la fonction KeUpdateRunTime décrémente le quantum et que celui-ci devient nul. Cela signifie que le temps d’exécution alloué au thread est écoulé et qu’il faut soit recharger son quantum, soit swapper sur le prochain thread. Désassemblons donc un peu KeUpdateRunTime :

kd> uf nt!KeUpdateRunTime
[...]
nt!KeUpdateRunTime+0x114:
804e343e sub     byte ptr [ebx+6Fh],3 ; on soustrait 3 au quantum
804e3442 jg      nt!KeUpdateRunTime+0x133 (804e345d) ; si plus grand que 0 on leave normalement la fct 

nt!KeUpdateRunTime+0x11a:
804e3444 cmp     ebx,dword ptr [eax+12Ch] ; eax=ffdff000=KPCR, compare le thread courant au idle thread
804e344a je      nt!KeUpdateRunTime+0x133 (804e345d) ; si egal on leave

nt!KeUpdateRunTime+0x122:
804e344c mov     dword ptr [eax+9ACh],esp ; met champ QuantumEnd du KPRCB à une valeur !=0 :p
804e3452 mov     ecx,2
804e3457 call    dword ptr [nt!_imp_HalRequestSoftwareInterrupt (804d7680)]

nt!KeUpdateRunTime+0x133:
804e345d pop     ebx
804e345e ret     4

Lorsque le quantum est nul, KeUpdateRunTime va mettre le ULONG QuantumEnd du KPRCB à true puis fait un appel à HalRequestSoftwareInterrupt. Cette fonction devant être appeler en convention fastcall, ses arguments sont passés par les registres. Ecx contient la valeur du seul argument de HalRequestSoftwareInterrupt qui est 2 (IRQL valant DISPATCH_LEVEL) dans le cas présent.

Regardons le code de HalRequestSoftwareInterrupt :

kd> uf hal!HalRequestSoftwareInterrupt
hal!HalRequestSoftwareInterrupt:
806f3884 mov     eax,1
806f3889 shl     eax,cl ; si cl=2 alors eax vaut 4
806f388b pushfd
806f388c cli
806f388d or      dword ptr ds:[0FFDFF028h],eax
806f3893 mov     cl,byte ptr ds:[0FFDFF024h] ; cl= current IRQL
806f3899 mov     eax,dword ptr ds:[FFDFF028h] ; IRR=4

d> !pcr
KPCR for Processor 0 at ffdff000:
    Major 1 Minor 1
	NtTib.ExceptionList: fcd395d4
	    NtTib.StackBase: fcd39df0
	   NtTib.StackLimit: fcd37000
	 NtTib.SubSystemTib: 00000000
	      NtTib.Version: 00000000
	  NtTib.UserPointer: 00000000
	      NtTib.SelfTib: 7ffde000

	            SelfPcr: ffdff000
	               Prcb: ffdff120
	               Irql: 0000001c
	                IRR: 00000004 <= !
	                IDR: ffff20f0
	      InterruptMode: 00000000
	                IDT: 8003f400
	                GDT: 8003f000
	                TSS: 80042000

	      CurrentThread: ffb47618
	         NextThread: 00000000
	         IdleThread: 80559020

806f389e and     eax,3 ; si eax=2 alors apres le ‘and’ eax=0
806f38a1 xor     edx,edx
806f38a3 mov     dl,byte ptr hal!SWInterruptLookUpTable (806f4784)[eax]

kd> db SWInterruptLookUpTable l 10
806f4784  00 00 01 01 02 02 02 02

eax=0 dionc après le mov, dl=0

806f38a9 cmp     dl,cl ; si IRQL > 0 on leave
806f38ab jbe     hal!HalRequestSoftwareInterrupt+0x30 (806f38b4)

hal!HalRequestSoftwareInterrupt+0x29:
806f38ad call    dword ptr hal!SWInterruptHandlerTable (806f476c)[edx*4]

hal!HalRequestSoftwareInterrupt+0x30:
806f38b4 popfd
806f38b5 ret

En détaillant un peu, on voit que HalRequestSoftwareInterrupt va mettre le champ IRR du KPCR à la valeur 4. Je ne connais pas la signification exacte de se champ mais je pense qu’il sert au mettre en place les DPC (Dispatch/Deferred Procedure Call). En effet lors de l’appel à cette fonction l’IRQL courant vaut CLOCK2_LEVEL, nous on veut obtenir une interruption software, or celles ci ne sont pas accessibles à un IRQL si élevé, elles devront donc être appelées lorsque celui ci redescendra. Ainsi le champ devrait donc permettre de dire qu’une demande d’interruption software est en attente.

Alors lorsque l’IRQL redescend, notre interruption software doit être activée. Hop straff-bunny-rail ! On disass halEndSystemInterrupt, la fonction appelé du hal juste avant qu’on quitte l’interruption int 30 avec Kei386EoiHelper.

kd> uf hal!HalEndSystemInterrupt
[...]
hal!HalEndSystemInterrupt+0x22:
806ee23e mov     byte ptr ds:[0FFDFF024h],cl ; met l'IRQL à la valeur qu'il avait avant l'interruption
806ee244 mov     eax,dword ptr ds:[FFDFF028h]
806ee249 mov     al,byte ptr hal!SWInterruptLookUpTable (806f4784)[eax]

kd> db SWInterruptLookUpTable l 10
806f4784  00 00 01 01 02 02 02 02

si IRR=4 alors al=2

hal!HalEndSystemInterrupt+0x37:
806ee253 ret     8

hal!HalEndSystemInterrupt+0x3a:
806ee256 add     esp,0Ch
806ee259 jmp     dword ptr hal!SWInterruptHandlerTable2 (806f4778)[eax*4]

kd> dps sWInterruptHandlerTable2
806f4778  806f3494 hal!KiUnexpectedInterrupt
806f477c  806f3bd1 hal!HalpApcInterrupt2ndEntry
806f4780  806f3a39 hal!HalpDispatchInterrupt2ndEntry

si eax=2 on call hal!HalpDispatchInterrupt2ndEntry

HalEndSystemInterrupt va lire la valeur IRR du KPCR et jump sur un interrupt handler si nécessaire. Dans notre cas c’est la fonction HalpDispatchInterrupt2ndEntry qui est appelé pour réaliser le DPC.

Now on regarde gentiment HalpDispatchInterrupt2ndEntry

hal!HalpDispatchInterrupt2ndEntry:
806f3a39 push    dword ptr ds:[0FFDFF024h]
806f3a3f mov     byte ptr ds:[0FFDFF024h],2 ; on définit l'IRQL à DISPATCH_LEVEL
806f3a46 and     dword ptr ds:[0FFDFF028h],0FFFFFFFBh
806f3a4d sti
806f3a4e call    dword ptr [hal!_imp__KiDispatchInterrupt (806ed428)]
806f3a54 cli
806f3a55 call    hal!HalpEndSoftwareInterrupt (806ee260)
806f3a5a jmp     dword ptr [hal!_imp_Kei386EoiHelper (806ed4f8)] ; quitte l'exception

Si on disass KiDispatchInterrupt :

kd> uf nt!KiDispatchInterrupt
nt!KiDispatchInterrupt:
804db874 mov     ebx,dword ptr ds:[0FFDFF01Ch]
804db87a lea     eax,[ebx+980h]
804db880 cli
804db881 cmp     eax,dword ptr [eax]
804db883 je      nt!KiDispatchInterrupt+0x2e (804db8a2)

nt!KiDispatchInterrupt+0x11:
804db885 push    ebp
804db886 push    dword ptr [ebx]
804db888 mov     dword ptr [ebx],0FFFFFFFFh
804db88e mov     edx,esp
804db890 mov     esp,dword ptr [ebx+988h]
804db896 push    edx
804db897 mov     ebp,eax
804db899 call    nt!KiRetireDpcList (804dbb8e)
804db89e pop     esp
804db89f pop     dword ptr [ebx]
804db8a1 pop     ebp

nt!KiDispatchInterrupt+0x2e:
804db8a2 sti
804db8a3 cmp     dword ptr [ebx+9ACh],0 ; si KPRC->QuantumEnd != on saute !
804db8aa jne     nt!KiDispatchInterrupt+0x8e (804db902)

nt!KiDispatchInterrupt+0x38:
804db8ac cmp     dword ptr [ebx+128h],0
804db8b3 je      nt!KiDispatchInterrupt+0x8d (804db901)

nt!KiDispatchInterrupt+0x41:
804db8b5 mov     eax,dword ptr [ebx+128h]

nt!KiDispatchInterrupt+0x47:
804db8bb sub     esp,0Ch
804db8be mov     dword ptr [esp+8],esi
804db8c2 mov     dword ptr [esp+4],edi
804db8c6 mov     dword ptr [esp],ebp
804db8c9 mov     esi,eax
804db8cb mov     edi,dword ptr [ebx+124h]
804db8d1 mov     dword ptr [ebx+128h],0
804db8db mov     dword ptr [ebx+124h],esi
804db8e1 mov     ecx,edi
804db8e3 mov     byte ptr [edi+50h],1
804db8e7 call    nt!KiReadyThread (804db6da)
804db8ec mov     cl,1
804db8ee call    nt!SwapContext (804db924)
804db8f3 mov     ebp,dword ptr [esp]
804db8f6 mov     edi,dword ptr [esp+4]
804db8fa mov     esi,dword ptr [esp+8]
804db8fe add     esp,0Ch

nt!KiDispatchInterrupt+0x8d:
804db901 ret

nt!KiDispatchInterrupt+0x8e:
804db902 mov     dword ptr [ebx+9ACh],0 ; le champ QuantumEnd est mit à 0
804db90c call    nt!KiQuantumEnd (804e7446)
804db911 or      eax,eax
804db913 jne     nt!KiDispatchInterrupt+0x47 (804db8bb)

nt!KiDispatchInterrupt+0xa1:
804db915 ret

KiDispatchInterrupt se charge d’exécuté les différents DPC existant à un IRQL de DISPATCH_LEVEL, puis elle vérifie la valeur du champ QuantumEnd du KPRCB. Si celui ci n’est pas nul alors il se produit un call à KiQuantumEnd.

Pour résumer un peu voici la call stack lors de l’appel à KiQuantumEnd.

kd> kv
ChildEBP RetAddr  Args to Child
f9a48908 804db911 806f3a54 00000000 f9a489bc nt!KiQuantumEnd (FPO: [Non-Fpo])
f9a4890c 806f3a54 00000000 f9a489bc 80567cea nt!KiDispatchInterrupt+0x9d (FPO: [Uses EBP] [0,0,3])
f9a4890c 00000000 00000000 f9a489bc 80567cea hal!HalpDispatchInterrupt2ndEntry+0x1b (FPO: [0,1] TrapFrame @ 00000000)

KiQuantumEnd à pour rôle de vérifier si la priorité du thread doit être augmenté et de renvoyé le next thread à lancer.
Plus loin KiDispatchInterrupt va call KiFindReadyThread et mettre dans la liste des thread ready le thread courant avec KiReadyThread. Puis intervient un changement de context pour charger non pas les registres du nouveau thread (pas de façon directe en tout cas) mais bien des éléments comme la nouvelle stack et recharger les nouveaux paramètres dans le KPCR.

Ainsi si on regarde un peu la liste des threads en state ready avec la commande !ready on remarque que la dernière fonction qu’ils ont appelé est celle qui à call SwapContext. Ce qui fait que, lors du changement de context le retn de la fin de la fonction SwapContext se situant dans la nouvelle stack relance l’exécution tu thread. Ainsi si le changement du thread à eu lieu après un DPC provenant de l’interruption int 30, la fonction SwapContext va gentiment retourner de KiDispatchInterrupt puis enfin de la fin de HalpDispatchInterrupt2ndEntry qui contient :

hal!HalpDispatchInterrupt2ndEntry:
[...]
806f3a4e call    dword ptr [hal!_imp__KiDispatchInterrupt (806ed428)]
806f3a54 cli
806f3a55 call    hal!HalpEndSoftwareInterrupt (806ee260)
806f3a5a jmp     dword ptr [hal!_imp_Kei386EoiHelper (806ed4f8)] ; recharge le contexte et quitte l'exception

Le magnifique jmp sur Kei386EoiHelper qui va recharger les registres de notre thread :] et le relancer.

C’est donc grâce à ce model que peut se mettre en place la préemption de threads ou le changement de contexte alors que le thread n’a pas finit son exécution. Remarquez que cela est loin d’être trivial à mettre en place mais se révèle extrêmes puissant par la suite. J’ai bien fait fumé mon IDA et mon KD pour retrouvez cela.

J’espère avoir été le plus clair possible, en cas de question n’hésitez pas à laissez un post ou à me contacter.

Entry Filed under: Non classé

9 Comments

  • 1. b0l0k  |  juin 10th, 2007 at 10:17

    Bel article ! Comme d’hab quoi :D


  • 2. Heurs  |  juin 10th, 2007 at 11:18

    Chapeau, t’as réussit à expliquer assez simplement une de parties extrèmement complexes du multi-threading, trés interessente en plus ! A quand l’explication sur le chainage des processus et leur switching ?

    Sinon j’ai une ptite question : quand une interruption est appelée, elle est toujours prise en charge dans le thread courrant par redirection de flux ?


  • 3. admin  |  juin 10th, 2007 at 11:30

    Il n’y pas vraiment de process switching, si jamais lors du switch, le prochain thread n’appartient pas au process courant, alors SwapThread va faire ce qu’il faut, comme mettre à jour le regitre cr3 contenant la page directory du futur thread (en effet il se situe dans un autre process) et aussi les informations du KPCR.

    Wi l’interruption se place dans le context du thread courant et l’interrompt donc.


  • 4. newsoft  |  juin 11th, 2007 at 05:54

    Ca n’est pas le BIOS qui provoque l’interruption à l’origine, mais un composant matériel (programmable) de la carte mère (de mon temps, un Intel 8253 mais ça a pu changer depuis). C’est d’ailleurs une interruption hardware qui est levée.

    On sent le p’tit jeune qui n’a jamais programmé son « PC Speaker » en assembleur sous MS-DOS pour faire de la synthèse vocale ;)

    A quand un code assembleur avec des IN et des OUT dans tes posts ?


  • 5. Ivan Groznij  |  juin 14th, 2007 at 18:29

    Salut Ivan le F0u,
    Moi je ne m’explique pas comment un « p’tit jeune qui n’en veut » comme toi, et qui touche sa bille sur ce type de sujet, est aussi nul en grammaire & orthographe.
    Pourtant, c’est toujours une histoire de langue / langage, et tu sais qu’on doit simplement respecter des règles et leurs exceptions … ;-)
    C’était un message à caratère informatif et amical d’un autre Ivan …


  • 6. admin  |  juin 14th, 2007 at 19:31

    Alalala, toujours la meme remarque. Si je fais autant d’erreurs c’est surement à force de trainer sur irc, c’est vraiment un endroit malsain …
    Mais d’un autre coté on pourrait dire que ca donne un certain « style » et puis franchement, j’aborde déjà des sujets assez compliqué comme ca, alors j’ai pas vraiment envie de me prendre la tête avec ce genre de détails. Bref j’avoue quand même que j’essaye de faire des efforts, mais Word ne corrige pas toutes les fautes ….


  • 7. overdose  |  juin 17th, 2007 at 01:37

    t sur ke c le bios qui declenche l’interruption ?
    ca utilise pas l’apic plutot ?


  • 8. texane  |  juin 21st, 2007 at 15:07

    depend si t as code l apic ou pas


  • 9. Tom  |  février 22nd, 2012 at 11:02

    Très bon post.
    Dommage qu’il y ait pleins de fautes d’orthographe…


Trackback this post


Calendar

janvier 2025
L Ma Me J V S D
« fév    
 12345
6789101112
13141516171819
20212223242526
2728293031  

Most Recent Posts