Crypto Museum · 2014. 9. 9. · Created Date: 1/19/1998 4:12:35 PM
Verdedigingsmechanismen tegen micro-architecturale crypto ...
Transcript of Verdedigingsmechanismen tegen micro-architecturale crypto ...
Jeroen Van Cleemput
crypto-analyseVerdedigingsmechanismen tegen micro-architecturale
Academiejaar 2008-2009Faculteit IngenieurswetenschappenVoorzitter: prof. dr. ir. Jan Van CampenhoutVakgroep Elektronica en informatiesystemen
Master in de ingenieurswetenschappen: computerwetenschappen Masterproef ingediend tot het behalen van de academische graad van
Begeleider: Bart CoppensPromotoren: prof. dr. ir. Koen De Bosschere, prof. dr. ir. Bjorn De Sutter
Voorwoord
Ik wil graag van de gelegenheid gebruik maken om mijn promotor, Prof. Dr. Ir. B. De Sutter,
en begeleider, Lic. B. Coppens, te bedanken voor de goede samenwerking. Ik kon steeds bij
hen terecht met vragen en voor feedback. Daarnaast had ik ook graag mijn promotor bedankt
voor het nalezen van mijn scriptie en het verschaffen van deskundig advies. Mijn ouders bedank
ik vervolgens voor de kans die ze me gaven om Computerwetenschappen te studeren, alsook
voor hun onvoorwaardelijke steun gedurende het schrijven van deze scriptie. Bovendien wil ik
iedereen, al dan niet expliciet vermeld, bedanken voor de steun gedurende mijn studies.
Jeroen Van Cleemput, mei 2009
Toelating tot bruikleen
“De auteur geeft de toelating deze scriptie voor consultatie beschikbaar te stellen en delen van
de scriptie te kopieren voor persoonlijk gebruik.
Elk ander gebruik valt onder de beperkingen van het auteursrecht, in het bijzonder met betrek-
king tot de verplichting de bron uitdrukkelijk te vermelden bij het aanhalen van resultaten uit
deze scriptie.”
Jeroen Van Cleemput, mei 2009
Verdedigingsmechanismen tegen
micro-architecturale crypto-analysedoor
Jeroen Van Cleemput
Scriptie ingediend tot het behalen van de academische graad vanburgerlijk ingenieur in de computerwetenschappen
Academiejaar 2008–2009
Promotor(s): Prof. Dr. Ir. B. DE SUTTER, Prof. Dr. Ir. K. DE BOSSCHEREScriptiebegeleider: Lic. B. COPPENS
Faculteit Toegepaste WetenschappenUniversiteit Gent
Vakgroep Elektronica en InformatiesystemenVoorzitter: Prof. Dr. Ir. J. VAN CAMPENHOUT
Samenvatting
Variabele uitvoeringstijd van programma’s is een bekend nevenkanaal voor micro-architecturalecrypto-analyse. Deze paper bespreekt drie compilertransformaties die de variatie in uitvoerings-tijd van enkele x86 instructies reduceren of elimineren.
Trefwoorden
crypto-analyse, compilertransformaties
Defense Mechanisms Against Micro-ArchitecturalCryptanalysis
Jeroen Van Cleemput
Supervisor(s): Bjorn De Sutter, Koen De Bosschere, Bart Coppens
Abstract—Variable program execution time is a well known side channelfor micro-architectural cryptanalysis. In addition to exi sting transforma-tions based on if-conversion this extended abstract suggests three additionaltransformations in the compiler backend that reduce or solve the variableexecution time of certain instructions for the x86 architecture. For the in-teger division instruction we suggest a transformation to remove variationin execution time due to early exit that is significantly faster than existingtechniques. Variable execution time due to load bypassing in the pipeline ismitigated by adding an artificial delay between a store and load instruction,this way load bypassing does not take place. Finally, an improved methodfor allocating dummy memory locations for load and store instructions inif-converted code is explained.
Keywords—micro-architectural cryptanalysis, compiler transformations
I. I NTRODUCTION
MICRO-ARCHITECTURAL cryptanalysis, unlike conven-tional cryptanalysis, is not about theoretical analysis of
cryptographic algorithms. Instead it uses extra information thatis not available in mathematical models. It observes the behav-ior of architectural components, signals and other properties likeexecution time. These side channels will show different proper-ties depending on the state a program is in. This knowledgecan be used to obtain more information about the current stateof the program. Because the state of the program at a certaintime depends on the input these side channels can effectivelybe used to obtain the original input or secret keys of encryptionalgorithms[5].
Coppens et al [1] have already proposed methods to eliminateall dependencies on secret keys from the control flow of a pro-gram, and in this paper we will propose new techniques to elim-inate timing-related dependencies from the data flow. Each ar-chitecture implements hardware components in a different way,therefor mitigation techniques against micro-architectural at-tacks can be very architecture specific. In this paper the focuslies on the x86 architecture, and more specific the Intel Core2platform. I will suggest three improvements focused on closingthe execution time of a program as a side channel.
II. VARIABLE LATENCY INTEGERDIVISION INSTRUCTIONS
. Integer division instructions on the Intel Core2 platformmakeuse of “early exit”[2]. The division has a variable execution timedepending on its operands. The necessary processor cycles arecomputed in advance and the division exits when this numberof cycles is reached. This can cause variable program executiontime if the divisions are dependant on program input.
. One possible solution is to make sure the variable latency ofthe division instruction does not affect the overall program ex-ecution time. For this we execute dummy code with a fixedexecution time in parallel with the integer division. Then guard
code is added to force the dummy code and the original divi-sion to run in parallel and to make sure the instructions after thedivision depend on both the dummy code and the original divi-sion. This way the following instructions will always have towait for the longest execution time, which will be the dummycode. If the dummy code uses one of the division operands asinput there is no need for guard code before the division. Andifthe result of the dummy code is 1, a simple multiplication withthe result of the original division can serve as guard code afterthe division. See fig. 1. This technique still allows a resource
Fig. 1. Dummy code is executed in parallel with the original division. Thenumber of multiplications in the dummy code needs to be determined foreach variable latency instruction.
contention side-channel attack [1]. An attacker can calculate in-teger divisions in second thread. The variable execution time ofthe divisions in the first thread will influence the amount of di-visions per timespan that can be calculated in the second thread.
. The transformation was tested on a modular exponentiational-gorithm with four different input sets[1]. 8 multiplications needto be added to 32bit code and 14 to 64bit code to be confidentthat there is no variation in execution time for the different sets.A 109% increase in execution time was measured an 78% in-crease for 64 bit code.
III. T HE PIPELINE BEHAVIOR OF LOAD INSTRUCTIONS
. Variable execution time can occur when a store instructionisfollowed by a load. If the memory disambiguation predictor [3]detects no dependency between the two memory addresses used,the load does not have to wait until the store finishes. The loadcan bypass the store instruction to obtain faster executiontimes.
Fig. 2. Execution time for different amounts of multiplications added (32bitcode). The curves converge when enough multiplications areadded
Depending on the memory addresses used in both instructionsadifferent execution time can be measured.
. By adding extra instructions between the two memory instruc-tions an artificial delay is created. If enough instructionsareadded between them, the store instruction will always be retiredbefore the load instruction is scheduled[4]. This way load by-passing does not occur anymore and there is no variation in ex-ecution time.
. Existing techniques[1] already reduce variable latency causedby load bypassing. The algorithm we implemented completelyremoves this variation in execution time. A performance de-crease of 14% was measured compared to the existing tech-niques.
IV. CACHE BEHAVIOR OF A PROGRAM
. When if-converting in the compiler backed, extra precautionsneed to be taken with load and store instructions. When loadsorstores are executed from a branch that is not taken these instruc-tions should not change the program state. Memory instructionsare therefor preceded by guard code that makes sure a dummymemory location is used when the current branch is not taken.In the existing solution [1], this guard code allocates one fixeddummy memory location on the stack per guarded instruction.Incertain situations this can cause unwanted behavior. For exam-ple in Fig. 3 the first loop fills the cache with data, the secondloop, depending on the value of a, will fill the cache with otherdata. Because of if-conversion, if a is false, the load instructionwill be executed 8000 times on the same dummy memory ad-dress and most data loaded in loop one will still be in the cache.Whereas a is true, 8000 different memory locations are loaded,and the cache is completely overwritten with new data. Thiscauses cache misses when the first loop is executed again. De-pending on the value of a, a different overall execution timecanbe measured.
. The desired behavior here would be that in both cases the en-tire cache is overwritten in the second loop. To do this, insteadof allocating one memory location per instruction, a block ofmemory the size of the cache is allocated. Then the dummy ad-
for(i=1;i<500000;i++){for(k=0;k<8000;k++){
result += *(mem+k);}for(j=0;j<8000;j++){
if(a){result += *(mem+10000+j);
}}
}
Fig. 3. This is a sample figure. The caption comes after the figure.
dress is calculated by using the x1 least significant bits of theoriginal address as an index in the allocated block of memory. xof course depends on the size of the allocated block of memory.If the original memory address is unknown, the dummy locationwill automatically point to the first index in the allocated block,which is the same behavior as the original way of allocatingdummy addresses.
. The code in figure 3 is tested with two different input sets. Inthe first set the conditional code is always executed, in the sec-ond set the conditional code is executed. Using the new alloca-tion method, the cache is on both cases completely overwrittenwith new data in the second loop and the cache behavior is iden-tical. After transforming the code there is no way to differentiatebetween the two input sets.
V. CONCLUSIONS
We were able to remove the variable latency caused by integerdivisions without adding a lot of extra overhead. Our transfor-mation is significantly faster than existing techniques[1]. 109%slower compared to 2300% slower for 32bit code and 78% com-pared to 1439% for 64bit code. Variable execution time causeby load bypassing is removed by adding an artificial delay be-tween store and load instructions. Furthermore the improvedmechanism for allocating dummy memory locations for loadsand stores in if-converted code removes unwanted cache behav-ior that causes variation in execution time in the case describedabove. Further research can be done to improve the performanceof these transformations, to find other cases where cache behav-ior can influence the execution time and to reduce the informa-tion leaked by the data cache.
REFERENCES
[1] Bart Coppens , Ingrid Verbauwhede, Koen De Bosschere, Bjorn De Sutter“Practical Mitigations for Timing-Based Side-Channel Attacks on Modernx86 Processors”
[2] Simcha Gochman, Avi Mendelson, Alon Naveh, Efraim Rotem“Introduc-tion to Intel Core Duo Processor Architecture”
[3] Jack Doweck “Inside Intel Core Microarchitecture and Smart Memory Ac-cess” “An In-Depth Look at Intel Innovations for Accelerating Executionof Memory-Related Instructions”
[4] J. Shen and M. Lipasti “Modern Processor Design: Fundamentals of Su-perscalar Processors” McGraw-Hill, 2005
[5] Onur Onur Acıimez, Jean-Pierre Seifert,etin Kaya Ko “Micro-architecturalcryptanalysis”
1x depends on the size of the allocated block of memory, for a cache size of32K 15 bits are necessary to index the whole block
INHOUDSOPGAVE i
Inhoudsopgave
1 Inleiding 1
1.1 Cryptografie en crypto-analyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.2 Micro-architecturale crypto-analyse . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.3 Bestaande verdedigingstechnieken . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.3.1 Blinding . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.3.2 Hardware . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.3.3 Broncode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.3.4 Compiler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.4 Probleemstelling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.5 Doel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.6 Overzicht thesis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2 Onderzoekscontext thesis 8
2.1 Controleverloop van een programma . . . . . . . . . . . . . . . . . . . . . . . . . 8
2.2 If-conversie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.2.1 Lussen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2.2.2 Geheugeninstructies . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2.2.3 Functieoproepen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2.2.4 x86 ISA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.3 Architectuur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.3.1 Low level virtual machine . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.4 Terkortkomingen van bestaande technieken . . . . . . . . . . . . . . . . . . . . . 15
2.5 Verloop thesis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
INHOUDSOPGAVE ii
3 Variabele uitvoeringstijd van delingsinstructies 17
3.1 Early Exit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
3.2 Verifieren van variabele uitvoeringstijd . . . . . . . . . . . . . . . . . . . . . . . . 18
3.3 Bestudeerde oplossingen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
3.3.1 Oplossing 1: Tijd van de deling constant maken . . . . . . . . . . . . . . . 20
3.3.2 Oplossing 2: Tijd van de deling geen rol laten spelen . . . . . . . . . . . . 21
3.4 Implementatie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
3.5 Resultaten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
4 Geheugenafhankelijkheden 30
4.1 Afhankelijkheden tussen geheugenadressen . . . . . . . . . . . . . . . . . . . . . . 30
4.1.1 Memory disambiguation en load bypassing . . . . . . . . . . . . . . . . . . 30
4.1.2 Verificatie van bestaande resultaten . . . . . . . . . . . . . . . . . . . . . 32
4.2 Bestudeerde oplossingen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
4.3 Implementatie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
4.4 Resultaten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
5 Cachegedrag 49
5.1 Cachegeheugen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
5.2 Invloed van cachegeheugen op uitvoeringstijd . . . . . . . . . . . . . . . . . . . . 49
5.3 Bestudeerde oplossingen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
5.4 Implementatie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
5.5 Resultaten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
6 Besluit en toekomstperspectieven 55
A Detailed information 57
A.1 CPU Details . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
A.2 Cache Details . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
INLEIDING 1
Hoofdstuk 1
Inleiding
1.1 Cryptografie en crypto-analyse
Crypto-analyse is een techniek in de cryptografie waarin men probeert geencrypteerde infor-
matie te ontcijferen, of de geheime sleutel van een algoritme te achterhalen. Crypto-analyse is
zeker geen recente ontwikkeling. Zo werden bijvoorbeeld tijdens de tweede wereldoorlog crypto-
analysetechnieken toegepast op de Enigma codeermachine. Deze gedecodeerde informatie had
een grote invloed op het verdere verloop van de oorlog. Omdat we evolueren naar een samen-
leving die meer en meer gebruik maakt van digitale technologieen is vandaag het belang van
cryptografie misschien nog veel groter. Grote hoeveelheden (persoonlijke) informatie worden
dagelijks over het internet verstuurd en steeds meer gegevens worden elektronisch opgeslaan.
De eigenaars van deze gegevens willen uiteraard dat deze informatie enkel kan bekeken worden
door bevoegde personen. Met andere woorden, de gebruikte encryptiemethodes moeten bestand
zijn tegen crypto-analysemethodes door derden. Crypto-analyse is ook niet beperkt tot enkel
encryptiealgoritmes, maar kan bijvoorbeeld ook toegepast worden op authenticatie- en authori-
satieprotocols. Zo kan een aanvaller zich voordoen als een van twee communicerende personen
zonder dat de andere dit door heeft. Crypto-analysten houden zich dus bezig met het zoeken van
zwakke punten in cryptografische algoritmes, en proberen aan de hand van deze zwakke punten
een aanval op te stellen. Het gaat dus om algemene theoretische en wiskundige analyses van
algoritmes. Bijvoorbeeld hoeveel tijd nodig is om een 1024 bit RSA modulus te factoriseren[14].
1.2 Micro-architecturale crypto-analyse 2
1.2 Micro-architecturale crypto-analyse
Conventionele crypto-analyse is gebaseerd op de theorie. Men gaat er van uit dat alle algoritmes
worden uitgevoerd op een abstracte Turing machine. In de praktijk is dit echter niet het geval.
De algoritmes worden uitgevoerd op een computer die beperkt is door de restricties van de
hardware waaruit hij is opgebouwd. Wanneer programma’s worden uitgevoerd laten ze een spoor
na in de verschillende signalen en componenten van de architectuur. Ook kan er een verschil
in waarneembaar gedrag worden gezien. De uitvoeringstijd bijvoorbeeld van een algoritme.
Deze sporen verschillen naargelang de invoer van het algoritme. Naargelang de invoer van het
programma zal een ander uitvoeringspad genomen worden in de code en zullen dus ook andere
instructies worden uitgevoerd. Dit wordt het controleverloop van het programma genoemd.
Als het algoritme bijvoorbeeld een waarde uit het geheugen nodig heeft wordt deze waarde
bijgehouden in het cachegeheugen van de architectuur, waardoor eventueel andere gegevens eerst
uit de cache moeten verwijderd worden om plaats te maken. Afhankelijk van de input van het
algoritme kan een andere waarde geladen worden, waardoor de cache zich in een verschillende
toestand zal bevinden. Een sprong in de code kan dan weer een wijziging veroorzaken in de
interne toestand van de sprongvoorspeller. De componenten, signalen en het waarneembare
gedrag waaruit informatie kan afgeleid worden over de interne toestand van een algoritme of
programma worden nevenkanalen genoemd. Door het observeren van de nevenkanalen kan men
dus extra informatie afleiden over de interne toestand van een algoritme dan wat in de theorie
mogelijk is. Micro-architecturale cryptanalyse gaat over de technieken om deze nevenkanalen te
observeren en zo meer informatie te verzamelen over de huidige toestand van het algoritme, de
sleutel en de invoer.
Micro-architecturale cryptanalyse werd eerst toegepast op relatief eenvoudige architecturen
zoals de smartcard. Door zijn klein rekenvermogen zijn variaties in uitvoeringstijd hier heel
duidelijk. Omdat de smartcard ook meestal fysisch in dezelfde ruimte aanwezig is als de aanvaller
kunnen nevenkanalen zoals elektromagnetische velden[16] en warmteontwikkeling ook gebruikt
worden. Meer recent werden ook technieken ontwikkeld die kunnen toegepast worden op huidige
algemeen toepasbare computerarchitecturen zoals de Intel processoren.
Hoe gaat een aanvaller nu juist te werk bij een micro-architecturale aanval[6]? De aanvallen
hier beschreven veronderstellen dat de aanvaller toegang heeft tot de machine waar de aan-
val wordt uitgevoerd (zowel fysiek als via secure shell), alhoewel hier ook uitzonderingen op
1.2 Micro-architecturale crypto-analyse 3
bestaan[15].
Een van de meer eenvoudige aanvallen is de datacache-aanval. Een cache is een buffer
tussen de processor en het hoofdgeheugen1. De toegangstijd tot de cache is veel sneller dan het
hoofdgeheugen. Wanneer de processor gegevens nodig heeft zal hij eerst controleren als ze niet
in de cache aanwezig zijn, voor het hoofdgeheugen aan te spreken. Encryptiealgoritmes zoals
AES (Advanced Encryption Standard) maken gebruik van een lookuptabel[14] die zich in het
geheugen bevindt. Wanneer gegevens uit deze lookuptabel worden gelezen worden die ook in
de datacache gekopieerd, zodat ze de volgende keer sneller toegankelijk zijn. De aanvaller zal
dan proberen gelijktijdig met de encryptiedraad een eigen draad op te starten. Wanneer een
aanvaller in een tweede draad gegevens uit een voldoende grote array leest zal de cache enkel
gegevens van de aanvaller bevatten. Als de tweede draad de processor weer vrijgeeft zal het
encryptiealgoritme weer delen van de cache overschrijven door gegevens uit de lookuptabel te
halen. Wanneer de controle weer wordt overgedragen naar de tweede draad, kan de aanvaller
door te meten hoe lang het duurt om elk gegeven uit de array te lezen als de gegevens zich
nog in de cache bevinden. Op deze manier kan de aanvaller weten welke gegevens in de cache
overschreven zijn door het encryptiealgoritme en dus welke gegevens uit de lookuptabel zijn
gebruikt. Als een aanvaller informatie kan verzamelen uit verschillende uitvoeringen van AES
met dezelfde sleutel, is het eenvoudig om hieruit de geheime sleutel af te leiden.
Een aanval op de instructiecache is gelijkaardig aan een datacache-aanval. De instructiecache
werkt zoals de datacache maar dan enkel voor instructies. Hier wordt echter informatie over het
controleverloop van het programma verzameld. Het controleverloop is het pad in de code die
wordt uitgevoerd (zie Figuur 2.1). Dit is afhankelijk van de invoer van het algoritme. Wanneer
informatie kan verzameld worden over het controleverloop levert dit dus ook informatie op
over de geheime sleutel. Een aanvaller zal in een tweede thread dummyinstructies uitvoeren.
Vervolgens wordt controle overgedragen aan het encryptiealgoritme. Wanneer de draad van de
aanvaller controle terug krijgt kan hij door te meten hoelang het duurt om elke instructie uit
te voeren informatie verzamelen over de instructies uitgevoerd in het encryptiealgoritme en dus
het controleverloop en uiteindelijk de geheime sleutel.
Een aanval op de sprongvoorspeller is al iets minder eenvoudig. Een van de krachtigste
aanvallen is de simple branch predition attack (SBPA)[17]. Deze aanval gebruikt de branch
target buffer (BTB)[18] om informatie te verzamelen. De BTB houdt voor elke spronginstructie1zie hoofdstuk 5 voor een gedetailleerde bespreking
1.3 Bestaande verdedigingstechnieken 4
informatie bij. Wanneer de data over een instructie niet aanwezig is in de BTB zal de sprongin-
tructie langer uitvoeren. Deze vertraging kan gemeten worden door een aanvaller. Hier kan een
aanvaller niet zoals in de vorige aanvallen zomaar dummycode uitvoeren of dummywaarden uit
het geheugen lezen. Een specifieke sequentie van intructies is nodig om misses te forceren in de
BTB.
1.3 Bestaande verdedigingstechnieken
Het doel van de verdedigingstechnieken is altijd dezelfde: Het afsluiten van de nevenkanalen.
Het is de bedoeling ervoor te zorgen dat er geen verschil meer kan gemeten worden op de
nevenkanalen wanneer een programma verschillende invoer verwerkt. Ongeacht de input moet
een nevenkanaal hetzelfde gedrag vertonen. Maar hoe worden programma’s nu beveiligd tegen
micro-architecturale crypto-analyse? Hier bestaan reeds een aantal technieken voor, deze kan je
opsplitsen in vier categorieen.
1.3.1 Blinding
Blinding[8] is een techniek die er voor zorgt dat het encryptiealgoritme nooit te maken heeft
met de echte input of output. Stel dat f() een encryptie functie is en we willen y = f(x)
berekenen. Eerst wordt een blinding functie E(x) toegepast op de invoer. Daarna wordt de
invoer geencrypteerd f(E(x). Tenslotte wordt de output van het algoritme gedecodeerd zodat
D(f(E(x))) = y 2. Het encryptiealgoritme wordt nooit uitgevoerd op de originele input. Een
aanvaller kan via nevenkanalen dan ook enkel informatie verzamelen over de geblindeerde invoer,
waar hij niet veel mee is. Uiteraard moet het blinding algoritme wel bestand zijn tegen micro-
architecturala crypto-analyse.
1.3.2 Hardware
Een manier om zich te verdedigen tegen micro-architecturale cryptanalyse is hardware zelf aan-
passen. De hardware wordt zodanig aangepast dat het niet meer mogelijk is om deze als een
nevenkanaal te gebruiken. Een relatief eenvoudig voorbeeld hiervan is de data cache uit schake-
len. Op deze manier zal elke geheugentoegang even lang duren en kan hieruit geen informatie
worden afgeleid. De entropie van de uitvoeringstijd is dan namelijk 0, en bevat volgens de2Bron: wikipedia
1.3 Bestaande verdedigingstechnieken 5
informatietheorie geen informatie. Op gespecialiseerde hardware voor cryptografische toepas-
singen worden de micro-architecturale componenten die op conventionele architecturen informa-
tie “lekken” anders geımplementeerd. Een voorbeeld hiervan is de AES (Advanced Encryption
Standard) instructieset van Intel [9]. Dit zijn zes instructies die volledige hardwareondersteuning
voor AES-encryptie ondersteunen. Waar conventionele encryptiealgoritmes een look-uptabel in
het geheugen raadplegen, worden hier gespecialiseerde instructies gebruikt. Vier voor encryptie
en decryptie en twee voor het sleuteluitbreidingsalgoritme. Deze instructies hebben een vaste
data-onafhankelijke uitvoeringstijd. Bovendien worden encryptie en decryptie volledig in hard-
ware uitgevoerd en zijn er geen geheugentoegangen nodig. Dit elimineert de data cache als
nevenkanaal. Dit is echter niet de focus van mijn thesis en ik zal mij daarom ook beperken tot
deze korte inleiding.
1.3.3 Broncode
Het is ook mogelijk om beveiligingen tegen micro-architecturale crypto-analyse rechtstreeks in
de broncode aan te brengen [7]. Zoals in sectie 1.2 al vermeld, wordt een verschillend pad in de
code gekozen naargelang de invoer. Deze technieken zijn voornamelijk gericht op het aanpassen
van het controleverloop van een programma in de broncode. Een voorbeeld hiervan is een
techniek voorgesteld in de paper “The program counter security model: Automatic detection
and removal of control-flow side channel attacks”[7]. Door het gebruiken van bitmasks kan
men alle controlestructuren uit de broncode verwijderen. Dit is echter ook niet de focus van
mijn thesis en voor een gedetailleerde uitleg van deze techniek verwijs ik naar [7]. Een van de
grootste nadelen van deze techniek is dat er een kans bestaat dat de uitgevoerde transformaties op
niveau van de broncode later worden weggeoptimaliseerd door de compiler. In het artikel wordt
vermeld dat de gegenereerde code nog moet geverifieerd worden om een sleutelonafhankelijk
controleverloop te garanderen. Meer nadelen van deze techniek staan beschreven in [2].
1.3.4 Compiler
De rest van mijn thesis zal handelen over compilertechnieken die gebruikt kunnen worden om te
verdedigen tegen crypto-analyseaanvallen. Aanpassen van een compiler heeft een aantal voorde-
len ten opzichte van de vorige technieken. Eerst en vooral is er geen nood aan nieuwe hardware.
Dit is zeer wenselijk omdat dan reeds bestaande architecturen kunnen gebruikt worden voor be-
veiligde cryptografische programma’s. Er moet dus geen gespecialiseerde hardware ontworpen
1.4 Probleemstelling 6
worden, wat de kosten van een project enorm kan drukken. Ook beschikt men over het volledig
gamma van optimalisaties van de compiler. Terwijl transformaties in de broncode kunnen teniet
gedaan worden door compileroptimalisaties, worden de transformaties hier pas uitgevoerd na-
dat de compiler de code heeft geoptimaliseerd. De transformties kunnen dus worden toegepast
zonder dat men zich zorgen moet maken dat deze weer weggeoptimaliseerd worden door de com-
piler. Dit kan er voor zorgen dat de prestatie iets hoger ligt dan bij broncodetransformatie, waar
men een aantal compileroptimalisaties zal moeten uitschakelen. Het is natuurlijk niet nodig om
de volledige code van een programma te transformeren. Enkel de code die afhankelijk is van
de input van het algoritme moet getransformeerd worden. De code voor de gebruikersinterface
heeft bijvoorbeeld totaal geen invloed op de prestatie van het encryptiealgoritme. In de praktijk
komt dit er op neer dat de programeur met behulp van annotaties zal aanduiden welke delen
van de code moeten getransformeerd worden.
1.4 Probleemstelling
Micro-architecturale crypto-analyse maakt het mogelijk om gemakkelijker geheime informatie
te achterhalen dan mogelijk is met conventionele crypto-analyse methodes. Zeker met de toe-
nemende digitalisering van de maatschappij is het belangrijk om cryptogragische algoritmes
hiertegen te beveiligen.
1.5 Doel
Het doel van mijn thesis is te kijken in hoeverre het mogelijk is om software aan te passen
om zich te verdedigen tegen micro-architecturale crypto-analyse. Er is reeds onderzoek gedaan
naar verdedigingstechnieken tegen micro-archtitecturale crypto-analyse. Zo is reeds een techniek
ontwikkelt die het controleverloop van een programma onafhankelijk maakt van de invoer[2].
Nadat het controleverloop onafhankelijk is kan naargelang de invoer nog steeds een ander gedrag
in de nevenkanalen worden waargenomen. Mijn thesis zal zich meer richten op het elimineren
van deze data-afhankelijkheden. Hierbij ga ik mij focussen op de x86 architectuur.
1.6 Overzicht thesis
In hoofdstuk 2 wordt een overzicht gegeven van de onderzoekscontext van mijn thesis. De reeds
behaalde resultaten binnen de vakgroep en mogelijke verbeteringen worden hier besproken. Ook
1.6 Overzicht thesis 7
wordt een korte introductie gegeven tot het compilerrraamwerk dat gebruikt wordt voor de
implementatie. In volgende hoofstukken worden de drie compilertransformaties besproken. Op
het einde komt er nog een samenvatting en mogelijke toekomstperspectieven.
ONDERZOEKSCONTEXT THESIS 8
Hoofdstuk 2
Onderzoekscontext thesis
In dit hoofstuk ga ik bespreken hoe compilertransformaties kunnen worden gebruikt om een
programma te verdedigen tegen micro-architecturale crypto-analyse. In de onderzoeksgroep
werd reeds onderzoek gedaan naar verdedigingstechnieken en ik zal beginnen met een overzicht
te geven van werk dat reeds verricht is. Daarop volgt wat uitleg over het compilerraamwerk dat
wordt gebruikt en een overzicht van mijn eigen onderzoek.
2.1 Controleverloop van een programma
De transformaties die uitgevoerd worden in de compiler hebben allemaal slechts een doel: Het
afsluiten van de nevenkanalen. Dit wil zeggen dat, onafhankelijk van de input, het programma
hetzelfde gedrag toont in de nevenkanalen. Een van de nevenkanalen waar het meeste aandacht
aan zal worden besteed is de uitvoeringstijd. Een programma bestaat uit een reeks van basic
blocks. Basic blocks zijn stukken code die altijd na elkaar worden uitgevoerd. Ze hebben dus
een ingangspunt en eindigen op een branch- of returninstructie. Daartussen bevinden zich ook
geen branch of return instructies, anders zou men de basic block moeten opsplitsen in twee
basic blocks. Naargelang de waarde van de branchinstructie wordt de controle overgedragen
naar een ander basic block van het programma. Een eenvoudig voorbeeld is te zien in Figuur
2.1. Hier wordt naargelang de sprongcondities in basic block A ofwel de code in basic block B
of C uitgevoerd, alvorens de controle wordt overgedragen naar basic block D. Het is duidelijk
dat de uitvoeringstijd van een programma afhankelijk is van het controleverloop. Niet alle in-
structies hebben even veel processorcyclussen nodig en niet alle basic blocks bevatten evenveel
instructies. Naargelang de sprongvoorwaarden zal het programma dus een verschillende uitvoe-
2.2 If-conversie 9
ringstijd hebben. Dit is zelfs nog duidelijker in Figuur 2.2 waar naargelang de sprongvoorwaarde
basic block B wordt uitgevoerd, of het doorvalpad wordt genomen naar basic block C. Als de
sprongvoorwaarde dan nog eens afhankelijk is van de input van het programma, kan men dus
een andere uitvoeringstijd meten naargelang de gegevens die men wil coderen.
Figuur 2.1: Eenvoudig voorbeeld van controleverloop in een programma met basic blocks. Af-
hankelijk van conditie c wordt ofwel blok B of C uitgevoerd.
2.2 If-conversie
Om het verschil in uitvoeringstijd door gevolg van het controleverloop van een programma
te elimineren wordt een techniek van if-conversie [10][11] gebruikt. If-conversie is een tech-
niek die in compilers wordt gebruikt om controleafhankelijkheden om te zetten naar data-
afhankelijkheden. Het komt er op neer dat in plaats van basic blocks conditioneel uit te voeren
naargelang de sprongvoorwaarden, de instructies zelf conditioneel worden uitgevoerd. Het pro-
gramma wordt dus omgezet van een aantal basic blocks waartussen controle wordt overgedragen
naar een enkele basic block, waarvan de instructies conditioneel worden uitgevoerd. Dit wordt
gedaan door predikaten toe te voegen aan de instructies, die aangeven of de instructie al dan
niet moet worden uitgevoerd. Een voorbeeld van if-converie is te zien in Listing 2.1 en 2.2.
2.2 If-conversie 10
Figuur 2.2: Eenvoudig voorbeeld van controleverloop in een programma met basic blocks met
doorvalpad
Listing 2.1: Origineel
instructie A
if(c) jump B2
B1: instructie B
jump B3
B2: instructie C
instructie D
jump B3
B3: ret
Listing 2.2: Na if-conversie.
instructie A
if(~c): instructie B
if( c): instructie C
if( c): instructie D
ret
If-conversie van de code heeft een aantal gevolgen, sommige positief en andere negatief.
Eerst en vooral valt op te merken dat if-conversie het programma inderdaad reduceert tot een
2.2 If-conversie 11
Figuur 2.3: Controleverloop van het programma in Figuur 2.1 na if-conversie. Het programma
is gereduceerd tot een basic block waarin de instructies conditioneel worden uitgevoerd.
basisblok 2.3. Onafhankelijk van de input wordt dus altijd het volledige basisblok doorlopen
zonder dat de controle wordt overgedragen door spronginstructies. Er zijn dus geen sprongen
meer die afhankelijk zijn van de invoer (behalve voor lussen, maar dit wordt later besproken).
De sprongvoorspeller kan bij gevolg niet meer gebruikt worden als nevenkanaal. Zoals eerder
uitgelegd houdt de sprongvoorspeller voor elke spronginstructie een teller bij die aanduidt als
de sprong al dan niet wordt genomen. Als er geen sprongen meer voorkomen in de code kan de
sprongvoorspeller uiteraard ook geen informatie meer lekken over de interne toestand van het
programma. De gepredikeerde instructies verbruiken ook altijd evenveel processortijd, ongeacht
welke waarde hun predikaat heeft. De uitvoeringstijd van het basicblok zou dus constant moeten
zijn, en op een aantal instructies na is dit ook zo. De uitzonderingen hier zijn instructies met
variabele uitvoeringstijd, deze worden verder besproken. De reden waarom if-conversie niet altijd
wordt toegepast in compiler heeft te maken met het grootste nadeel, namelijk het prestatieverlies.
Het doel van een compiler is om het programma zo performant mogelijk te maken. If-conversie
doet eigenlijk juist het omgekeerde. In plaats van zo weinig mogelijk instructies zo snel mogelijk
uit te voeren, worden na if-conversie alle instructies uitgevoerd, zelfs als deze niet moeten worden
uitgevoerd. Dit heeft een enorme invloed op de prestatie van een programma.
If-conversie reduceert het verschil in uitvoeringstijd en sluit het gebruiken van de sprongvoor-
speller als nevenkanaal compleet af. Daartegenover staat natuurlijk het grote prestatieverlies.
If-conversie alleen lost enkel het probleem van variabele uitvoeringstijd op door gevolg van het
controleverloop van het programma. Er zijn een aantal codestructuren die na if-conversie nog
steeds voor variabele uitvoeringstijd zorgen. In de volgende secties worden deze codestructuren
verder besproken.
2.2 If-conversie 12
2.2.1 Lussen
Een basisblok kan een lust bevatten die afhankelijk van de input van een algoritme een variabel
aantal keer worden uitgevoerd. Het is ook mogelijk dat er op elk moment uit de lus wordt
gesprongen, bijvoorbeeld met een break in een C lus. Dit soort lussen worden niet-manifeste
lussen genoemd. Deze lussen zorgen ervoor dat, zelfs na if-conversie, het programma nog steeds
een variabele uitvoeringstijd heeft. Een mogelijke oplossing hiervoor is dat de gebruiker bij een
lus een vaste bovengrens aanduidt zodat de lus altijd een vast aantal keer wordt uitgevoerd.
Als de lus bijvoorbeeld drie keer moet worden doorlopen en de bovengrens tien is, dan worden
de laatste zeven iteraties uitgevoerd met de predikaten van de instructies op vals. Er moet
wel opgemerkt worden dat niet-manifeste lussen erg weinig voorkomen in encryptiealgoritmes.
De lussen zijn meestal afhankelijk van een constante waarde in het algoritme, bijvoorbeeld een
sleutellengte. Het is echter niet altijd gemakkelijk voor de compiler om te detecteren als een
lus manifest is. Daarom kan de programeur met behulp van annotaties aanduiden als een lus al
dan niet manifest is. Omdat de meeste lussen in encryptiealgoritmes reeds manifest zijn, is het
voldoende om de lussen te annoteren.
2.2.2 Geheugeninstructies
Extra voorzorgsmaatregelen moeten genomen worden bij instructies die naar het geheugen schrij-
ven of van het geheugen lezen. Er moet voor gezorgd worden dat deze instructies een dummy
geheugenadres gebruiken als ze zich in een tak bevinden die niet moet worden uitgevoerd. Ze
zouden anders de interne toestand van het programma kunnen wijzigen, of zelfs fouten veroor-
zaken als naar ongeldige adressen wordt geschreven. Dit wordt gedetailleerder beschreven in de
hoofdstukken over geheugengedrag.
2.2.3 Functieoproepen
Als in een basisblok een functieoproep staat, moet deze altijd worden opgeroepen. Zelfs als de tak
waarin de functie oproep staat niet wordt uitgevoerd, ander zou er een verschil in uitvoeringstijd
ontstaan. De lokale variabelen in de functie mogen echter niet naar de globale variabelen van
het programma gekopieerd worden als de functie is opgeroepen in een tak die niet mag worden
uitgevoerd. Hiervoor wordt een extra argument meegegeven met de functie. Een booleaanse
waarde die aangeeft als de functie effectief moet worden uitgevoerd. Als die waarde vals, is
worden de locale variabelen niet naar de globale gekopieerd en worden loads en stores op dummy
2.2 If-conversie 13
adressen uitgevoerd. Op deze manier kan de functie uitgevoerd worden zonder dat het iets aan
de toestand van het programma verandert.
2.2.4 x86 ISA
If-conversie is een techniek die ontwikkeld werd voor VLIW architecturen [12][11]. In de vorige
voorbeelden werd ervan uitgegaan dat alle instructie conditioneel kunnen worden uitgevoerd.
Dit is echter niet het geval voor de x86 ISA. De x86 ISA beschikt slechts over een instructie die
conditioneel kan worden uitgevoerd, de conditionele move (cmov). Dit wil niet niet zeggen dat
if-conversie niet kan toegepast worden op deze architectuur. In de paper “Practical Mitigations
for Timing-Based Side-Channel Attacks on Modern x86 Processors” [2] wordt een techniek
beschreven om if-conversie toe te passen op een x86 architectuur. Zie ook Listing 2.3.
1. Vooraleer een basisblok samen te voegen ervoor zorgen dat de instructies uit het basisblok
enkel op lokale (tijdelijke) variabelen werken.
2. Voeg safeguardcode toe aan instructies die de globale toestand van het programma kunnen
veranderen als ze worden uitgevoerd. Deze safeguardcode moet ervoor zorgen dat de
instructies kunnen worden uitgevoerd ook als hun predikaat op vals staat. Denk hierbij
aan loads, stores die voor neveneffecten kunnen zorgen als ze worden uitgevoerd in een niet
genomen tak. Er moet ook voorkomen worden dat delingen door 0 worden uitgevoerd, dit
zal excepties geven en het programma crashen.
3. Voeg aan het einde van het basisblok conditionele move-instructies toe die de lokale vari-
abelen naar globale variabelen kopieeren als het pad genomen is.
Op deze manier kan if-conversie ook toegepast worden op de x86 architectuur.
Listing 2.3: If-conversie voor x86 ISA. Dit is een voorbeeld genomen uit “Practical Mitigations
for Timing-Based Side-Channel Attacks on Modern x86 Processors” [2]
tmp_a = a;
if(~c) tmp_a = dummy_location;
*tmp_a = 10;
tmp_y = y;
if ( c ) tmp_y = 1;
tmp_d = x / tmp_y;
2.3 Architectuur 14
tmp_b = 10;
if(c) d = tmp_d;
if(~c) b = tmp_b;
2.3 Architectuur
De technieken die in het vervolg van mijn thesisboek beschreven worden, zijn architectuur
specifiek. Technieken die werken op een core2 platform kunnen bijvoorbeeld totaal geen in-
voed hebben op een pentium M platform. Het komt zelfs voor dat een bepaald probleem niet
voorkomt op een ander platform. Caches kunnen een verschillende grootte hebben, of anders
geımplementeerd zijn. Instructieselectie[18] kan op een andere manier gebeuren op verschillende
platformen en sprongvoorspellers kunnen ook een totaal ander ontwerp hebben. Een algeme-
ne oplossing in software voor het probleem van micro-architecturale crypto-analyse bestaat dus
(nog) niet. Daarom is het nuttig om platform per platform te zoeken naar mogelijke oplossingen.
Een architectuurspecifieke aanpak heeft ook voordelen ten opzichte van een architectuuronaf-
hankelijke benadering [2]. Programma’s die voor een specifieke architectuur zijn gecompileerd
zullen veel performanter zijn dan een programma die architectuuronafhankelijk is gecompileerd.
Enkel de transformaties die voor de specifieke doelarchtitectuur nodig zijn, zullen worden uitge-
voerd. Dit is vergelijkbaar met de manier waarop compilers zoals gcc ook architectuurspecifieke
optimalisaties kunnen uitvoeren. Het levert extra prestatiewinst op, maar de code is niet meer
overdraagbaar naar andere architecturen. Ik heb gekozen voor het intel core2 platform omdat
dit een veel gebruikt modern platform is. Het heeft een geavanceerde geheugenarchitectuur die
zeer geschikt is voor mijn experimenten en het verifieren van reeds bestaande resultaten. Om
de architectuurspecifieke aard aan te tonen heb ik ook een aantal experimenten uitgevoerd op
een Pentium M platform om resulaten te vergelijken. Verder werden de tests uitgevoerd op een
32bit linux besturingssysteem. Zie bijlage A voor meer details over de testsetup.
2.3.1 Low level virtual machine
Voor de implementatie van de tranformaties wordt gebruik gemaakt van de Low Level Virtual
Machine (LLVM) compiler infrastructuur. Via een C/C++ front-end wordt de C-code omgezet
naar een tussentijdse representatie van de code die eigen is aan LLVM. Dit wordt de inter-
mediate representation genoemd (IR). Op deze IR worden dan de optimalisatie transformaties
2.4 Terkortkomingen van bestaande technieken 15
Figuur 2.4: Schema van de llvm architectuur, gebaseerd op een figuur in “Introduction to the
LLVM Compiler Infrastructure” [4]
uitgevoerd. Daarna wordt deze IR vertaald naar architectuuronafhankelijke machinecode waar
transformaties op machine code niveau kunnen worden uitgevoerd. Ten laatste wordt de ma-
chineonafhankelijke code omgezet naar code voor een bepaalde doelarchitectuur, waar eventueel
nog architectuurspecifieke transformaties kunnen worden uitgevoerd. Met behulp van plugins
is het mogelijk om transformaties toe te voegen aan dit compilerraamwerk. Er is reeds een
plugin ontworpen binnen de vakgroep. Deze implementeert reeds het if-conversie algoritme en
alle codestructuren uit sectie 2.2 die nog voor variatie zorgen na if-conversie. In de rest van
de tekst zal ik gewoon over de balanceringsplugin spreken. Deze transformatie, ook wel pass
genoemd, wordt uitgevoerd op niveau van de mid-level optimizer in Figuur 2.4 en wordt dus
uitgevoerd op de IR. Voor mijn eigen experimenten zal ik deze plugin uitbreiden en aanpassin-
gen maken in de codegenerator. Deze uitbreidingen zullen per transformatie in detail besproken
worden in de volgende hoofdstukken. Zoals al eerder vermeld, moeten de transformaties niet op
alle code worden uitgevoerd. Het is mogelijk om annotaties mee te geven in de broncode. De
balanceringsplugin kan dan aan de hand van die annotaties beslissen als er transformaties op
een functie moeten worden uitgevoerd. Een voorbeeld van een annotatie voor C-code is te zien
in Listing 2.4. Dit is zeker geen overzicht van alle functionaliteit van LLVM, maar slechts de
functionaliteit waarvan ik gebruik heb gemaakt voor mijn onderzoek.
Listing 2.4: Een voorbeeld van annotaties in C.
int __attribute__ (( annotate("balance"))) myFunction(int* a, int* b);
2.4 Terkortkomingen van bestaande technieken
Terwijl if-conversie de variatie in uitvoeringstijd sterk doet verminderen en het gebruik van de
sprongvoorspeller als nevenkanaal uitsluit, zijn er toch nog enkele instructies die voor variatie
kunnen zorgen. In het vervolg van mijn thesis zal ik deze instructies verder onderzoeken en
enkele mogelijke verdedigingsmechanismen voorstellen. Een overzicht van de problemen die zich
2.5 Verloop thesis 16
nog stellen na if-conversie:
• Er doen zich wel nog data-afhankelijkheden voor, namelijk instructies met een variabele
uitvoeringstijd. Een eerste voorbeeld dat ik heb onderzocht, is de deling van gehele getal-
len. De gehele deling heeft een variabele uitvoeringstijd naargelang zijn operandi. Deze
wordt besproken in hoofdstuk 3.
• Vervolgens heb je instructies die lezen of schrijven naar het geheugen, zoals load- en store-
instructies. Instructies die naar het geheugen schrijven of uit het geheugen lezen, kunnen
op twee manieren de uitvoeringstijd beınvloeden. Ten eerste kan de processor afhankelijk-
heden detecteren tussen twee opeenvolgende instructies die het geheugen aanspreken. Als
deze instructies hetzelfde geheugenadres nodig hebben, kan het zijn dat de ene instructie
op de andere moet wachten tot dat de andere instructie klaar is. Als ze niet hetzelfde ge-
heugen adres nodig hebben kunnen deze instructies eventueel parallel uitgevoerd worden.
Deze afhankelijkheden worden besproken in hoofstuk 4.
• Ten laatste zijn er ook nog afhankelijkheden die veroorzaakt worden door het cachegedrag.
Om te voorkomen dat loads en stores de toestand van een programma veranderen wanneer
ze worden opgeroepen uit een niet genomen tak wordt voor deze instructies guardcode
toegevoegd. Deze guardcode zorgt ervoor dat de instructies op dummy geheugenadressen
worden uitgevoerd. Als de tak niet genomen is, zal een store-instructie schrijven naar
een dummy locatie en als de tak wel genomen is zal de store-instructie schrijven naar de
originele geheugenlocatie. Het schrijven naar de dummy locatie kan hier dan bijvoorbeeld
een cache-miss zijn, terwijl het schrijven naar de originele geheugenlocatie een cache-hit
is. Dit is een bijkomende bron van variabiliteit in de uitvoeringstijd. Dit wordt verder in
detail besproken in hoofdstuk 5.
2.5 Verloop thesis
De basis voor technieken in verband met controleverloop was reeds gelegd, zoals gepubliceerd in
[2]. Enkele ideen uit deze thesis werden reeds opgenomen in [2] tijdens revisie van dat artikel.
Ik zal telkens expliciet vermelden welke ideen ik zelf aangebracht heb, en welke niet.
VARIABELE UITVOERINGSTIJD VAN DELINGSINSTRUCTIES 17
Hoofdstuk 3
Variabele uitvoeringstijd van
delingsinstructies
In dit hoofdstuk wordt de gehele delingsinstructie in meer detail besproken. Afhankelijk van
zijn operandi heeft de deling een andere uitvoeringstijd. Dit biedt nog mogelijkheden om de uit-
voeringstijd van een programma als nevenkanaal te gebruiken. Zeker ook omdat cryptografische
algoritmes veel gebruik maken van gehele delingen, denk bijvoorbeeld aan modulo-bewerkingen
in modulaire exponentiatie. Er worden twee soorten oplossingen voorgesteld. In de eerste wordt
ervoor gezorgd dat de uitvoeringstijd van de deling constant is. De tweede oplossing zorgt er-
voor dat de variabele uitvoeringstijd geen invloed heeft op de totale uitvoeringstijd van een
programma.
3.1 Early Exit
De gehele delinginstructies op de Core2 duo hebben geen constante uitvoeringstijd [1]. Ze maken
gebruik van mogelijkheden tot ’early exit’. Het aantal cyclussen die nodig is om de deling te
berekenen wordt op voorhand bepaald en de deling wordt dan vervroegd afgerond als dit aantal
cyclussen is bereikt. Dit zorgt er niet voor dat de maximum uitvoeringstijd van de deling
vermindert, maar de gemiddelde uitvoeringstijd van een aantal random delingen zal wel dalen.
Omdat de uitvoeringstijd van de gehele deling afhankelijk is van zijn operandi en de operandi
mogelijks afhankelijk zijn van de programmainvoer kan de totale uitvoeringstijd nog altijd kan
varieren. Testen tonen aan dat op het pentium M platform geen early exit optreedt. Early exit
is dus specifiek voor het core2 duo platform.
3.2 Verifieren van variabele uitvoeringstijd 18
3.2 Verifieren van variabele uitvoeringstijd
Eerst en vooral ben ik begonnen met het verifieren of er wel degelijk een early exit optreedt
bij deling van gehele getallen op de Core2 Duo processor. Hiervoor heb ik een stukje C-code
geschreven zoals in Listing 3.1. De resultaten van de code voor gehele getallen (sdiv) voor
verschillende delers en deeltallen is te zien op Figuur 3.1 1. Op het eerste zicht treedt een early
exit op wanneer het deeltal kleiner is dan de deler. Meer specifiek kan dat verklaard worden
door te kijken naar de binaire representatie van deler en deeltal.
Figuur 3.1: Early Exit bij delingen van gehele getallen. Deze figuur stelt een 100x99 matrix voor
van uitvoeringstijden van gehele delingen. Horizontaal staan de deeltallen, vertikaal de delers.
Delingen in het rode oppervlak gebruiken geen early exit, delingen in het groene oppervlak wel.
Als het deeltal dus voldoende groter is dan de deler, treedt er een early exit op. Een uitzondering
hierop is de deling door 1, die altijd early exit gebruikt.
Door Figuur 3.1 te bestuderen kan een patroon afgeleid worden om te bepalen wanneer een
early exit zal optreden: Er wordt gekeken naar de meest significante 1 bit van deler en deeltal
van de deling. Als de meest significante 1 bit van het deeltal minder significant is dan de meest1Uitvoeringstijden voor positieve gehele getallen (natuurlijke getallen) zijn vergelijkbaar.
3.2 Verifieren van variabele uitvoeringstijd 19
significante van de deler, dan treedt er een early exit op. Als de deler dus voldoende groter is
dan het deeltal, zal er een early exit optreden.6396 → early exit6496 → geen early exit
Binaire representatie van de getallen:
96 : 1100000
64 : 1000000
63 : 0111111
Listing 3.1: C-code om early exit te testen.
unsigned int unsigned_division_test(unsigned int a,unsigned int b){
int i,j=0;
for(i=0;i <1000000000;i++){
j+= (a/b);
}
return j;
}
int signed_division_test(int a,int b){
int i,j=0;
for(i=0;i <100000000;i++){
j+= (a/b);
}
return j;
}
3.3 Bestudeerde oplossingen 20
3.3 Bestudeerde oplossingen
3.3.1 Oplossing 1: Tijd van de deling constant maken
Deling uitwerken in apparte functie
Een eerste optie die reeds bestaat, is de deling uit te werken door enkel gebruik te maken
van instructies met een vaste uitvoeringstijd [2] zoals bij een staartdeling. Deze code wordt
dan in een aparte functie geplaatst en alle delingsinstructies worden vervangen door een een
functieoproep.Dit zorgt ervoor dat de deling altijd een vaste uitvoeringstijd heeft. Omdat de
deling helemaal moet worden herschreven is het prestatieverlies wel enorm[2].
Deling herschrijven naar deling zonder early exit
Zoals in de vorige sectie reeds besproken, treedt er geen early exit op als het deeltal voldoende
groter is dan de deling. Het is mogelijk om met een paar eenvoudige wiskundige bewerkingen
de deling zo te herschrijven zodat dit het geval is. Op deze manier treedt er nooit een early exit
op.
A
B=
A + B −B
B(3.1)
=A + B
B− 1 (3.2)
=2(A + B)
2B− 1 (3.3)
We beginnen met ervoor te zorgen dat het deeltal altijd groter is dan de deler door het
deeltal te vervangen door de som van deler en deeltal en 1 van het totaal af te trekken zoals in
3.2. Omdat de deler mogelijk waarde 1 heeft en bij deling door 1 altijd een early exit optreedt,
worden de deler en deeltal nog eens vermenigvuldigd met 2 om dit probleem op te lossen (3.3).
De deling op deze manier herschrijven zorgt ervoor dat er nooit een early exit optreedt. Ook
presteert deze code niet veel slechter dan een normaal geval waar geen early exit optreedt. Het
aantal cyclussen nodig voor een gehele deling is namelijk veel groter dan de cyclussen nodig voor
het optellen en aftrekken van gehele getallen.
De formule 3.3 heeft echter problemen met afrondingen. Door het deeltal te vervangen door
de som van het deeltal en twee keer de deler zoals in 3.6 bekomen we een formule die exact
dezelfde resultaten geeft als de originele deling en die niet veel slechter presteert dan een deling
3.3 Bestudeerde oplossingen 21
zonder early exit.
A
B=
A + 2B − 2B
B(3.4)
=A + 2B
B− 2 (3.5)
=2(A + 2B)
2B− 2 (3.6)
Een groot nadeel van deze methode is dat de programeur niet meer beschikt over het volledige
bereik van gehele getallen. Het deeltal 2(A + 2B) zal namelijk zorgen voor een integeroverflow
bij grote waarden van A en B. De programeur moet dus rekening houden met het feit dat
de code deze transformatie zal ondergaan. De transformatie is niet zomaar toepasbaar op alle
broncode. Deze formule op zich biedt ook enkel een oplossing voor gehele delingen zonder teken.
De formule kan waarschijnlijk wel aangepast worden om te werken met gehele getallen met teken,
maar omdat deze aanpak in de praktijk niet erg nuttig is door gevolg van overflow, ben ik hier
niet verder op in gegaan. Deze twee nadelen zorgen ervoor dat deze methode in de praktijk
onbruikbaar is.
3.3.2 Oplossing 2: Tijd van de deling geen rol laten spelen
Een andere mogelijke oplossing is om ervoor de zorgen dat variabele uitvoeringstijd van de
deling geen invloed heeft op de globale uitvoeringstijd van het programma. Dit kan bekomen
worden door extra instructies toe te voegen aan de code en de processor te proberen forceren
om deze code parallel uit te voeren met de deling. Deze extra instructie moeten een vaste
uitvoeringstijd hebben, en tesamen langer duren dan de originele deling (Figuur 3.2). Daarna
moeten nog twee afhankelijkheden worden gecreerd om ervoor te zorgen dat de processor de
twee stukjes code parallel uitvoert. Eerst moet ervoor worden gezorgt dat zowel de deling als de
dummyinstructies afhankelijk zijn van eenzelfde waarde, zodat ze ongeveer gelijktijdig uitgevoerd
worden. Vervolgens moet ervoor gezorgd worden dat de code volgend op de deling afhankelijk
is van zowel de resultaten van de deling als de resultaten van de dummycode. Op deze manier
maakt het niet uit hoe lang de deling duurt. De volgende intructies moeten wachten tot het
resultaat van de dummycode is berekend. De instructies die de afhankelijkheden creeren zullen
in de rest van de tekst guardcode worden genoemd. Door het toevoegen van dummycode en
guardcode hebben de effecten van early exit geen invloed meer op de globale uitvoeringstijd van
het programma.
Een probleem bij deze aanpak is het vinden van geschikte guardcode. Een processor heeft
3.3 Bestudeerde oplossingen 22
Figuur 3.2: Parallel uitvoeren van code met vaste uitvoeringstijd.
verschillende rekeneenheden voor de verschillende soorten instructies. Een scheduler zorgt er-
voor dat de instructies in de correcte rekeneenheid worden geplaatst. Dit wordt schematisch
voorgesteld in Figuur 3.3. Om de code parallel te kunnen uitvoeren moet dus gekozen worden
voor instructies die op een andere rekeneenheid terecht komen. Dit kan getest worden door
een deling in parallel uit te voeren die nooit een early exit geeft. Er wordt dan nog altijd een
variabele uitvoeringstijd gemeten. Dit klopt met de veronderstelling, de twee delingen worden
gescheduled op dezelfde rekeneenheid, waardoor ze dus niet in parallel kunnen worden uitge-
voerd. Meetresultaten tonen aan dat de uitvoeringstijd van de extra deling gewoon bij de totale
uitvoeringstijd wordt opgeteld. Omdat de gehele deling een instructie is die een groot aantal
processorcyclussen nodig heeft om te berekenen, is het vinden van dummycode om parallel uit
te voeren niet triviaal. Load- en Store-instructies hebben ook een groot aantal processorcyclus-
sen nodig omdat deze moeten wachten op de tragere cache of het geheugen. Omdat loads en
stores zelf een variabele uitvoeringstijd hebben (zie verder), zijn dit dus geen geschikte kandida-
ten. Een andere mogelijkheid is het gebruiken van een aantal instructies die een kleiner aantal
processorcyclussen nodig hebben, zoals add, sub en mul.
Ook het vinden van geschikte guardcode is niet triviaal. Om ervoor te zorgen dat de code
die op de deling volgt zowel afhankelijk is van de deling als de dummycode die in parallel wordt
3.3 Bestudeerde oplossingen 23
Figuur 3.3: Een scheduler zend instructies naar de correcte rekeneenheden. Dit is een figuur
gebaseerd op [3].
uitgevoerd, moet de guardcode de resultaten van beide berekeningen op een of andere manier
combineren. Het gecombineerde resultaat moet dan ook door de daaropvolgende instructies
gebruikt worden. Wordt dit resultaat niet verder gebruikt, dan zal de dummycode en guard-
code hoogstwaarschijnlijk weggeoptimaliseerd worden door een van de volgende stappen in de
compiler. Het resultaat van de combinatie van de twee delingen moet dus hetzelfde zijn als het
resultaat van de originele deling. Als ervoor gezorgd wordt dat de resultaten van de dummycode
die in parallel wordt uitgevoerd altijd 1 is, kunnen in de guardcode gewoon de twee resultaten
met elkaar vermenigvuldigd worden. Dit resultaat kan dan door de volgende instructies gebruikt
worden in plaats van het resultaat van de originele deling.
Een volgend probleem is het zoeken van guardcode en dummy berekeningen die niet worden
weggeoptimaliseerd in volgende optimalisatiestappen. Gebruiken van constanten is niet aan te
raden. De compiler zal dan tijdens de compilatie al kunnen berekenen dat de uitkomst van de
dummycode 1 is en dat de originele deling met 1 wordt vermenigvuldigd. Dit heeft als gevolg
dat alle dummycode wordt weggeoptimaliseerd. Zelfs minder eenvoudige berekeningen zonder
constanten zoals een register Xor’en met zichzelf en er 1 bij optellen, of logische shift van 32
posities en erna 1 bij optellen worden door de compiler weggeoptimaliseerd.
Het volgende stuk code zoals in Figuur 3.4 wordt getoond, wordt niet weggeoptimaliseerd
door de compiler. Op een van de operandi van de originele deling wordt een rekenkundige ver-
schuiving naar rechts (SAR, shift arithmetic right) van 31 posities uitgevoerd. Een rekenkundige
verschuiving behoudt de meest linkse tekenbit van een getal in two’s complementrepresentatie.
Het resultaat van de verschuiving is dus 0 of -1. Hierna wordt er een een OR met 1 uitgevoerd
op het vorige resultaat. Er zijn weer twee mogelijke resultaten: -1 en 1. Daarna wordt dit
resultaat een aantal keer met zichzelf vermenigvuldigd tot de gewenste vaste uitvoeringstijd is
bereikt. Het resultaat van de dummy berekening is uiteraard 1, zodat de afhankelijkheid met
3.3 Bestudeerde oplossingen 24
een eenvoudige vermenigvuldiging kan worden opgelost.
Het is natuurlijk mogelijk om pas in een latere fase, nadat alle optimalisaties zijn uitgevoerd,
deze transformaties te doen. Dan moet er geen rekening gehouden worden met code die wordt
weggeoptimaliseerd. Omdat er in de vakgroep reeds een plugin geımplementeerd is die een aantal
transformaties uitvoert, heb ik besloten om deze te proberen uitbreiden in plaats van een nieuwe
plugin te implementeren die later in het compileerproces wordt uitgevoerd.
Figuur 3.4: Een voorbeeld van dummycode die reeds in IR kan worden geımplementeerd. De
dummycode die in parallel wordt uitgevoerd, bestaat uit een shift met behoud van teken van
een van de operandi van de originele deling. Daarna wordt de laatste bit van dit resultaat op
1 gezet zodat enkel +1 of −1 als resultaat kan worden bekomen. Dan wordt dit resultaat een
aantal keer met zichzelf vermenigvuldigd om een voldoende lange uitvoeringstijd te bekomen.
Omdat het resultaat van de dummycode altijd 1 is, volstaat een eenvoudige vermenigvuldiging
met het resultaat van de originele deling om een afhankelijkheid te creeren in de guardcode.
Een nadeel van deze methode ten opzichte van de methode die de deling in een aparte fucntie
uitrekent[2] is dat het nog steeds mogelijk is om door middel van een ander soort aanval toch
nog informatie te verzamelen, namelijk resource contention. De gehele delingsinstructie is niet
gepijplijnd. Dit wil zeggen dat deze instructies moeten wachten tot de vorige delingsinstructie
afgerond is om te uitgevoerd te worden. Ook worden delingsinstructies op eenzelfde rekeneen-
3.4 Implementatie 25
heid uitgevoerd[13]. Als een aanvaller in een tweede draad constant gehele delingsinstrucies
uitvoert en meet hoeveel hij er in een bepaald tijdsinterval kan uitvoeren, kan hij ook informatie
verzamelen over het algoritme dat in de eerste draad loopt. Als in de eerste draad een algoritme
wordt uitgevoerd met enkel early exit delingen zal de aanvaller meer delingen kunnen uitvoeren
dan wanneer de delingen in de eerste draad geen early exit gebruiken. De variatie in uitvoe-
ringstijd wordt nu gemeten op het programma van de aanvaller in plaats van op het beveiligde
programma. Deze aanval kan niet gebruikt worden op de twee andere methodes die hierboven
zijn beschreven. Deze gebruiken immers geen delingen met variabele uitvoeringstijd meer.
3.4 Implementatie
Om deze transformatie te implementeren heb ik de bestaande balanceringsplugin uitgebreid.
De bestaande plugin is een ModulePass, wat er op neer komt dat deze pass optimalisaties kan
uitvoeren op alle broncode van het programma. Een ander voorbeeld van een pass is bijvoorbeeld
een FunctionPass, deze is dan beperkt tot de code van een enkele functie. De modulepass voert
optimalisaties uit op de IR-representatie van LLVM. Als de pass een gehele delingsinstructie
tegenkomt, wordt een functie opgeroepen die de hierboven beschreven transformaties uitvoert.
3.5 Resultaten
Het algoritme die de deling parallel uitvoert met een aantal multiplicaties wordt getest met
behulp van een modulair exponentiatie algoritme (Listing 3.2). Om resultaten te kunnen ver-
gelijken is hetzelfde modulair exponentiatie algoritme gebruikt als in “Practical Mitigations
for Timing-Based Side-Channel Attacks on Modern x86 Processors” [2]. Het algoritme wordt
uitgevoerd met vier verschillende invoer sets:
• Enkel 0 set, bijna alle bits van de exponent zijn nul, enkel de twee meest significante bits
zijn een. Dit zorgt ervoor dat het de variabele result niet constant blijft. Alle andere
bits op nul zorgt ervoor dat de conditionele code in Listing 3.2 maar twee keer wordt
uitgevoerd. Dit pattroon geeft accurate sprongvoorspellingen door de processor.
• Enkel 1 set, alle bits uit de exponent zijn een. Dit zorgt ervoor dat de conditionele code
in Listing 3.2 elke iteratie wordt uitgevoerd. Dit pattroon geeft ook accurate sprongvoor-
spellingen door de processor.
3.5 Resultaten 26
• Regulier, de helft van de bits is een en de andere helft is nul, volgens een constant patroon.
De conditionele code wordt in de helft van de iteraties uitgevoerd en de sprongvoorspelling
is accuraat.
• Random, de helft van de bits is een en de andere helft is nul, maar deze keer is de volgorde
bepaald door een pseudo-randomgenerator. De conditionele code wordt in de helft van de
gevallen uitgevoerd maar de sprongvoorspelling is veel minder accuraat dan bij de reguliere
set.
Listing 3.2: C-code die modulaire exponentiatie implementeert. Dit stuk code wordt gebruikt
om de tranformatie te testen.
result = 1;
do{
result = (result*result) % n;
if (( exponent >>i) & 1)
result = (result*a) % n;
i--;
} while (i >=0);
Figuur 3.5 toont de resultaten van de originele balanceringsplugin[2], de balanceringsplugin
die de deling in een andere functie uitwerkt en de versie die extra vermenigvuldigingen parallel
uitvoert. Het modulair exponentiatie algoritme wordt uitgevoerd op zowel 32, 64 als 256 bit
getallen. Voor de 256 bit versie is de invloed van de delingsinstructie verwaarloosbaar en geeft
de originele balanceringsplugin al goede resultaten.
In de twee meest rechtse kolommen staan de gegevens van de twee transformaties die de
variabele uitvoeringstijd van de deling elimineren. 3.5.a) Toont de gemiddelde uitvoeringstijd
over twintig uitvoeringen. Figuur 3.5.b) toont de standaardafwijking. Het is duidelijk dat de
transformatie die code parallel uitvoert veel performanter is dan de transformatie die de deling
in een aparte functie uitwerkt: 109% trager dan de originele code ten opzichte van 2300% trager
voor 32 bit code en 78% trager ten opzichte van 1439% trager voor 64 bit code.
In Figuur 3.5.c) worden de p-waarden van de t-test gegeven. P-waarden tonen aan hoe
gelijkaardig twee samples zijn. Lage p-waarden wil zeggen dat twee samples niet door eenzelfde
bron zijn gegenereerd. Hoge p-waarden tonen aan dat er geen verschil kan opgemerkt worden
tussen twee samples. De p-waarden in de tabel tonen aan dat if-conversie alleen niet voldoende
3.5 Resultaten 27
is voor de 32 en 64 bit versies van het modulair exponentiatie algoritme. Na het elimineren van
variabele uitvoeringstijd van de deling kan wel met voldoende zekerheid besloten worden dat er
geen verschil meer meetbaar is tussen de verschillende invoer sets.
Het aantal vermenigvuldigingen die moet worden toegevoegd is afhankelijk van het aantal bits
dat gebruikt wordt om de getallen voor te stellen. Figuur 3.6 toont de p-waarden van de t-test
in functie van het aantal vermenigvuldigingen die in parallel wordt uitgevoert. Voor het 32bit
modulair exponentiatie algoritme kan er vanaf acht vermenigvuldigingen voldoende vertrouwen
gezegd worden dat de uitvoeringstijden gelijk zijn. Omdat de gemiddelde uitvoeringstijd van
de 64bit deling hoger is moeten er meer vermenigvuldigingen worden toegevoegd om evenhoge
p-waarden te krijgen.
De transformatie die vermenigvuldigingen parallel uitvoert is dus een veel performanter
alternatief voor de transformatie die de deling in een apparte functie berekent. De p-waarden
tonen aan dat er met voldoende zekerheid kan beslist worden dat de uitvoeringstijden voor de
verschillende sets gelijk zijn. Het enig nadeel is dat de techniek geen bescherming biedt tegen
een resource contention aanval.
3.5 Resultaten 28
Fig
uur
3.5:
Tab
elm
etde
gem
idde
lde
uitv
oeri
ngst
ijd,
stan
daar
dafw
ijkin
gen
p-w
aard
enva
nde
t-te
st.
De
drie
eers
tko
lom
men
tone
n
mee
tres
ulta
ten
van
het
orig
inel
eal
gori
tme,
een
reed
sbe
staa
ndal
gori
tme
die
if-co
nver
sie
toep
ast,
enee
nbe
staa
ndal
gori
tme
die
deva
riab
ele
uitv
oeri
ngst
ijdva
nde
delin
gel
imin
eert
[?].
De
vier
deko
lom
dege
geve
nsva
nde
tran
sfor
mat
iedi
eva
riab
ele
uitv
oeri
ngst
ijdva
nde
delin
g
elim
inee
rtui
tdi
tpa
per.
Het
isdu
idel
ijkve
elpe
rfor
man
ter
enge
eft
deze
lfde
resu
ltat
en.
3.5 Resultaten 29
Figuur 3.6: p-waarden van de t-test voor een verschillend aantal vermenigvuldigingen.
GEHEUGENAFHANKELIJKHEDEN 30
Hoofdstuk 4
Geheugenafhankelijkheden
Zoals eerder vermeld hebben load- en store-instructie ook een variabele uitvoeringstijd. Dit is
het gevolg van twee factoren. Ten eerste het pijplijngedrag van de loadinstructie en ten tweede
het cache gedrag van geheugeninstructies. In dit hoofdstuk wordt het pijplijngedrag van de
loadinstructie in meer detail bekeken. Ook wordt er een oplossing voorgesteld om variatie in
uitvoeringstijd ten gevolge van dit gedrag te vermijden.
4.1 Afhankelijkheden tussen geheugenadressen
4.1.1 Memory disambiguation en load bypassing
Intel processors maken gebruik van een out-of-orde geheugeneenheid (Inside Intel Core Micro-
architecture and Smart Memory Acces[3]). Dit wil zeggen dat de processor in staat is om
onafhankelijke geheugeninstructies in elke volgorde uit te voeren. Wanneer een store en een
loadinstructie na elkaar moeten worden uitgevoerd zal de memory disambiguation predictor
controleren als er een afhankelijkheid is tussen de twee geheugenadressen. Indien er geen af-
hankelijkheid wordt gedetecteerd, de voorspeller beslist dus dat de geheugeninstructies niet het
zelfde adres gebruiken, kan de loadinstructie voor de store-instructie worden uitgevoerd. Loadin-
structies voor store-instructies in de pijplijn plaatsen wordt load bypassing genoemd. Als er wel
een afhankelijkheid wordt gedetecteerd en de twee instructies (mogelijk) hetzelfde geheugenadres
gebruiken, moet de loadinstructie wachten tot de waarde in het geheugen beschikbaar is. Load
bypassing kan hier niet worden toegepast.
Sinds de core2 architectuur maakt Intel gebruik van meer geavanceerde technieken voor out-
of-order geheugentoegangen [3]. Waar in oudere Intel architecturen een load nooit voor een store
4.1 Afhankelijkheden tussen geheugenadressen 31
naar een ongekend adres wordt geplaats, kan dit nu wel. Indien later een conflict gedetecteerd
wordt, wordt de pijplijn gecleared. Voor elke loadinstructie wordt er een gesatureerde teller bij-
gehouden die aangeeft als de load voor een store in de pijnlijn mag worden geplaatst. Indien een
conflict wordt gedetecteerd, wordt deze teller op nul gezet zodat de volgende keer de instructie
wordt uitgevoerd, er geen bypassing meer optreedt.
Als niet naar het volledige geheugenadres wordt gekeken bij het controleren van afhankelijk-
heden tussen twee geheugeninstructies spreekt men van pessimistic load bypassing. In dit geval
kan het dus gebeuren dat er geen bypassing gebeurt, zelfs wanneer er geen afhankelijkheid is
tussen de twee instructies. De reden waarom niet het volledige geheugenadres wordt gebruikt
kan performantie zijn, of het gebruik van eenvoudigere hardware.
Load bypassing zorgt dus voor variatie in de uitvoeringstijd naargelang het geheugenadres
waar naartoe wordt geschreven. In de praktijk kan dit bijvoorbeeld voorkomen wanneer in een
lus achtereenvolgens naar een vaste locatie wordt geschreven en uit een willekeurige positie in een
array wordt gelezen. Het is gemakkelijk in te zien dat door disambiguation niet altijd eenzelfde
uitvoeringstijd zal gemeten worden.
In de testcode voor modulaire exponentiatie komt geen variatie in uitvoeringstijd voor ten
gevolge van load bypassing. Dit wil natuurlijk niet zeggen dat die niet voorkomt in ande-
re cryptografische algoritmes. Daarom wordt in dit hoofdstuk gebruik gemaakt van een stuk
C-code die speciaal gemaakt is om variatie in uitvoeringstijd door load bypassing te meten
(Listing 4.1). In een lus wordt na elkaar een waarde 2 naar geheugenplaats a geschreven
en dan de waarde uit geheugenplaats b gelezen. In de experimenten die volgen zal deze co-
de dan uitgevoerd worden met a en b die wijzen naar verschillende plaatsen in een array.
4.1 Afhankelijkheden tussen geheugenadressen 32
Listing 4.1: C-code die pessimistic load bypassing aantoont. Eerst wordt de constante waarde
2 naar de geheugenlocatie b geschreven, daarna wordt de waarde op geheugenlocatie a opgeteld
bij result. Verschillende offsets tussen a en b geven een verschillende uitvoeringstijd.
int mem3(int* a, int* b){
int result;
int j;
for(j=0;j<LOOPS;j++){
*b= 2;
result += *a;
}
return result;
}
Het is belanrijk om op te merken dat het verschil in uitvoeringstijd niets te maken heeft
met het feit dat er een cache-miss zou kunnen optreden. Het stukje code gebruikt slechts
twee geheugenadressen die wijzen naar gehele getallen van 4 byte groot. Dit past uiteraard in
het cache geheugen zodat er geen cache-misses voorkomen. Ook wordt de lus een voldoende
groot aantal keer uitgevoerd. Hierdoor zijn de sprongvoorspeller en de memory disambiguation
voorspeller “opgewarmd”. De tellers in deze componenten die beslissen of een sprong moet
genomen worden of een load voor een store mag uitgevoerd worden, zullen snel verzadigd zijn
en alle voorspellingen zullen correct gebeuren. De effecten van een verkeerde voorspelling in de
eerste iteraties zijn door het grote aantal iteraties verwaarloosbaar.
4.1.2 Verificatie van bestaande resultaten
Er is binnen de vakgroep reeds onderzoek gedaan naar de gevolgen van load bypassing [2]. Zij
kwamen tot het besluit dat enkel naar bit 2 tot 5 wordt gekeken om te controleren als er een
afhankelijkheid bestaat tussen twee geheugeninstructies. Dit komt er dus op neer dat wanneer
de offset tussen een store en een load modulo 64 gelijk is aan 0 of 4. Of in formulevorm:
(a− b)%64 = 0
(a− b)%64 = 4
Om dit te verifieren heb ik een test geschreven die het stukje code uit Listing 4.1 een aantal
keer uitvoert. Er wordt een array van gehele getallen van vier byte groot in het geheugen
4.1 Afhankelijkheden tussen geheugenadressen 33
geınitialiseerd en de pointers met a en b wijzen bij elke uitvoering naar een verschillende plaats
in de array. Zie Figuur 4.1. Initieel wijzen beide pointers naar dezelfde locatie. Bij elke
volgende uitvoering wordt a een plaats (4 bytes) opgeschoven in de array. Er wordt dus telkens
een verschillende offset tussen de twee geheugenadressen gebruikt. De uitvoeringstijden voor de
verschillende offsets zijn te zien in Figuur 4.2. Deze resultaten zijn inderdaad identiek aan de
resultaten die reeds gepubliceerd zijn[2].
Figuur 4.1: Pointers a en b wijzen naar verschillende plaatsen in een array, zodat verschillende
offsets tussen geheugenadressen worden getest.
Bij een tweede test wordt pointer b elke iteratie een plaats opgeschoven in plaats van a,
zoals aangetoond in Figuur 4.3. De grootte van de offset tussen de twee geheugeninstructies
is dus dezelfde als in de vorige test. Je zou dus verwachten dat ook op dezelfde plaatsen
afhankelijkheden worden gedetecteerd. Dit is echter niet het geval. Resultaten van de tweede
test zijn te zien op Figuur 4.4. Hier zijn er ook afhankelijkheden tussen de twee geheugenadressen
gedetecteerd wanneer de offset modulo 64 gelijk is aan 8 en 12. Hieruit kan besloten worden
dat ook de richting van de offset een rol speelt bij het bepalen van afhankelijkheden tussen twee
geheugeninstructies. Bij een positieve offset, zoals het eerste geval, worden afhankelijkheden
gedetecteerd bij modulo64 gelijk aan 0 en 4, bij een negatieve offset worden afhankelijkheden
4.1 Afhankelijkheden tussen geheugenadressen 34
Figuur 4.2: Uitvoeringstijden voor verschillende offsets tussen de pointers a en b zoals in Figuur
4.1 aangegeven. Er is duidelijk te zien dat er mogelijke afhankelijkheden tussen de geheugen-
locaties worden gedetecteerd als de offset tussen de twee adressen modulo 64 gelijk is aan 0 of
4.
gedetecteerd bij modulo 64 gelijk aan 0,4,8 en 12.
(a− b)%64 = 0
(a− b)%64 = 4
(a− b)%64 = 8
(a− b)%64 = 12
Reeds gepubliceerde resultaten[2] geven dus maar een geval van een aantal mogelijke gevallen
en is dus niet volledig correct. Er moet dus een nieuwe verklaring gezocht worden die de
formules verenigt. Hiervoor heb ik een derde test gedaan waar niet enkel de offset tussen de
geheugenlocaties varieert, maar ook de plaats van de geheugenlocaties een rol speelt. Deze test
is identiek aan de eerste maar in plaats van enkel verschillende offsets tussen geheugenadressen
te testen, wordt ook telkens een andere beginpositie in de array genomen. In de eerste reeks
testen wijzen a en b initieel naar positie 0 in de array, in de tweede reeks initieel naar 1, dan
naar 2, enz. Resultaten van deze test zijn te zien op Figuur 4.5. Deze figuur toont het verband
aan tussen de geheugenadressen en de uitvoeringstijd. Horizontaal wordt de verschillende offset
tussen de geheugenadressen getoond, zoals in de vorige tests. En elke rij stelt dezelfde test voor
maar met een ander startpunt in de array. De eerste rij komt dus overeen met de resultaten van
4.1 Afhankelijkheden tussen geheugenadressen 35
Figuur 4.3: Pointers a en b wijzen naar verschillende plaatsen in een array, zodat verschillende
offsets tussen geheugenadressen worden getest. In tegenstelling tot 4.1 wijst hier pointer a elke
test naar het zelfde geheugenadres
de eerste test. In het rooster worden telkens de laatste 6 bits van de tweede geheugentoegang
gegeven. Het geheugenadres van de eerst geheugen toegang wordt gegeven in de kolom waar de
offset nul is. De rode vakjes duiden aan dat er een afhankelijkheid is gemeten. Bij groene vakjes
is geen afhankelijkheid gemeten. Er is duidelijk een ander gedrag wanneer andere beginposities
in de array worden gebruikt.
Dit gedrag herhaalt zich voor offsets groter dan 60 en shifts groter dan 15 (dit komt ook neer
op een shift van 60 bytes). Bijgevolg kan besloten worden dat enkel de laatste 6 bits gebruikt
worden om afhankelijkheden te bepalen. De minst significante bit heeft nummer 0, de meest
significante 5. Omdat integers vier bytes groot zijn, zijn bits 0 en 1 altijd 0 in deze test en
dus irrelevant. Bij de eerste test worden ook afhankelijkheden gemeten als de offset modulo
64 gelijk is aan 4. Dit kan als oorzaak hebben dat bit 3 ook genegeerd wordt bij controle van
afhankelijkheden. Dit blijkt ook het geval te zijn bij deze test. Dan blijven er enkel nog bit
3 tot 5 over. Als deze 3 bits worden geınterpreteerd als een binair getal, x1 voor de eerste
geheugentoegang en x2 voor de tweede geheugentoegang. Dan wordt er een afhankelijkheid
gedetecteerd als x2 gelijk is, of een lager dan x1. Omdat dit gedrag zich modulo 64 herhaalt,
is het voldoende om te controleren of dit klopt voor de gevallen in Figuur 4.5, alle andere
geheugenadressen zijn identiek aan een geval in Figuur 4.5.
Het is niet duidelijk waarom geheugenafhankelijkheden op deze manier worden berekend.
Slechts enkele bits van twee geheugenadressen vergelijken vereist duidelijk minder complexe
4.2 Bestudeerde oplossingen 36
Figuur 4.4: Uitvoeringstijden voor verschillende offsets tussen de pointers a en b zoals in Figuur
4.3 aangegeven. Hier worden ook mogelijke afhankelijkheden tussen geheugenadressen gedetec-
teerd wanneer de offset tussen de twee adressen modulo 64 gelijk is aan 0,4,8 of 12.
hardware dan wanneer het volledige adres moet worden vergeleken. De bovenstaande methode
om geheugenafhankelijkheden te detecteren maakt dit dan weer complexer omdat meer logica
nodig is dan enkel een aantal bits vergelijken. Bovendien geeft het ook meer vals positieve
afhankelijkheden, wat de prestatie negatief beınvloedt. Om wat meer verduidelijking te vragen
over de manier waarop geheugenafhankelijkheden worden berekend heb ik een e-mail gestuurd
naar Intel. Ik heb hier helaas nog geen antwoord op gehad.
4.2 Bestudeerde oplossingen
De metingen uit de vorige paragraaf werden uitgevoerd op code die gecompileerd is met een
gewone compiler (gcc), zonder het toepassen van if-conversie of andere transformaties om ne-
venkanalen af te sluiten. Het is ook nuttig om te meten of deze effecten nog steeds voorkomen
nadat if-conversie is uitgevoerd en guardcode is toegevoegd voor load- en store-instructies. Er
kan namelijk een groot verschil zijn tussen de machinecode met en zonder balancering.
De uitvoeringstijden van het stukje code in Listing 4.1 voor en na balancering worden getoond
in Figuur 4.6. Buiten het prestatieverschil is ook te zien dat er al heel wat minder variatie in
uitvoeringstijd is. Enkel bij offsets van 0 en 4 tussen de twee geheugenlocaties is er nog een
verschil te zien en dit herhaalt zich niet meer bij offsets die gelijk zijn modulo 64. Dit is een
grote verbetering ten opzichte van de niet gebalanceerde code, waarbij ongeveer 1/4 van de
offsets een afhankelijkheid wordt gedetecteerd.
4.2 Bestudeerde oplossingen 37
Fig
uur
4.5:
Uit
voer
ings
tijd
enva
nhe
tst
ukje
code
inL
isti
ng4.
1.B
ijro
devl
akke
nw
ordt
een
afha
nkel
ijkhe
idge
dete
ctee
rd,
bij
groe
neni
et.
4.2 Bestudeerde oplossingen 38
Figuur 4.6: Verschil in uitvoeringstijd tussen gebalanceerde en niet gebalanceerde code uit
Listing 4.1. Balanceren reduceert de variatie in uitvoeringstijd. Bij offsets 0 en 4 is wel nog een
kleine vertraging te zien.
Een ander stuk code, dat identiek hetzelfde geheugengedrag heeft als de code in Listing 4.1
is te zien in Listing 4.2. Hier worden geen twee pointers naar geheugenplaatsen meegegeven. In
de plaats wordt een pointer naar het begin van de array gebruikt tesamen met een index die de
offset tussen de twee geheugenadressen bepaalt.
Listing 4.2: C-code met exact hetzelfde geheugengedrag als het stukje code in Listing 4.1.
Hier worden geen twee pointers naar geheugenplaatsen meegegeven. In de plaats wordt een
pointer naar het begin van de array gebruikt tesamen met een index die de offset tussen de twee
geheugenadressen bepaalt.
int mem1(int a, int* mem){
int result = 0;
int j;
for(j=0;j<LOOPS;j++){
*mem = 2;
result += *(mem+a);
}
return result;
}
Na balanceren van deze code is er geen variatie in uitvoeringstijd meer te zien zoals bij de vorige
test. Om de oorzaak hiervan te bekijken is het handig om eerst eens te bekijken wat de gevolgen
4.2 Bestudeerde oplossingen 39
zijn van het toevoegen van safeguardcode op niveau van assembler code. Zoals eerder vermeld
worden loads en stores uitgevoerd op dummy adressen als de tak waar de instructie zich in be-
vindt niet wordt uitgevoerd. Dit wordt in assembler code geimplementeerd met een conditionele
move instructie. De safeguardcode zorgt er dus voor dat er meer assemblerinstructies staan tus-
sen twee opeenvolgende geheugeninstructies waardoor deze ook minder invloed op elkaar hebben.
Listing 4.3: Assembly voor mem3 balanced. Dit stuk code kopieert de waarde 2 naar het
geheugen en telt een waarde uit het geheugen op bij het register edi. De cmov instructies zijn de
safeguardcode die naargelang de tak genomen is ofwel het originele of het dummygeheugenadres
selecteren.
cmovl %ecx ,%ebx
movl $0x2 ,(% ebx)
lea (%esp),%ebx
cmovl %edx ,%ebx
add (%ebx),%edi
Listing 4.4: Assembly voor mem1 balanced. Omdat het geheugen hier via een index wordt
geraadpleegd is er een extra lea (load effective address) instructie nodig om de exacte geheugen-
plaats te berekenen. Dit zorgt voor extra intructies tussen de twee geheugeninstructies.
cmovl %ecx ,%ebx
movl $0x2 ,(% ebx)
lea (%ecx ,%edx ,4),%ebx
lea (%esp),%ebp
cmovl %ebx ,%ebp
add 0x0(%ebp),%edi
Listing 4.3 toont een stuk van de machine-instructies voor de code in Listing 4.1. Deze code heeft
nog steeds een variabele uitvoeringstijd. Listing 4.4 bevat de machine-instructies gegenereerd
uit Listing 4.2 waar geen variatie meer voorkomt in de uitvoeringstijd. Een meer gedetailleerde
uitleg bij de code vindt u in het bijschrift. Omdat de het tweede stuk code (4.2) een geındexeerde
toegang maakt tot de array in tegenstelling tot een rechtstreekse wijzer naar een geheugenplaats,
is er een extra lea(load effective addres) instructie nodig om het effectieve geheugenadres te be-
rekenen. In het tweede geval(4.4) worden er dus meer machine-instructies uitgevoerd tussen de
twee geheugeninstructies als in het eerste geval (4.3). Het zou dus kunnen dat het toevoegen
4.2 Bestudeerde oplossingen 40
Figuur 4.7: Verschil in uitvoeringstijd tussen gebalanceerde en niet gebalanceerde code uit
Listing 4.2. Na balancering is er geen verschil meer te zien in uitvoeringstijd.
van extra instructies ervoor zorgt dat er voldoende tijd (of processorcycli) tussen de twee ge-
heugeninstructies is gepasseerd zodat wanneer de tweede instructie het geheugen nodig heeft, de
eerste instructie al voltooid is. Op deze manier hebben de twee geheugeninstructies geen invloed
meer op elkaar, zelfs als er eerder een afhankelijkheid zou zijn gedetecteerd door de memory
disambiguation predictor.
De assembler code die door de compiler gegenereerd wordt, bevat niet altijd evenveel instruc-
ties tussen de twee geheugeninstructiesplaatsen. En het is moeilijk om de compiler een bepaalde
combinatie van instructies te laten genereren door aanpassingen te maken in de C-code. Om
het effect van het aantal instructies tussen de twee geheugenplaatsen te testen, heb ik een stukje
inline assembler code geschreven met dezelfde functionaliteit. Het is dan veel gemakkelijker om
specifieke assembler instructies toe te voegen en hiermee te experimenteren.
4.2 Bestudeerde oplossingen 41
Listing 4.5: Inline assemblercode om te experimenteren met verschillend aantal instructies tussen
de twee geheugeninstructies. Esi wordt hier als telregister gebruikt en ebx en ecx bevatten
pointers naar de twee geheugenlocaties. Zoals in de vorige stukjes C-code wordt hier ook een
lus uitgevoerd die achtereenvolgens naar het geheugen schrijft en uit het geheugen leest.
asm ( "movl $0 , %%esi;" //esi teller
"movl $0, %%eax;"
"movl %0, %%ebx;" //a in ebx
"movl %1, %%ecx;" //b in ecx
"jmp overloop ;"
"loop:"
"movl $2, (%%ebx);"
"addl (%%ecx),%%eax;"
"incl %%esi;"
"overloop :"
"cmpl $1000000000 ,%%esi;"
"jl loop;"
:
: "r" (ap), "r" (bp)
: "%esi","%ebx","%eax","%ecx");
Het doel is het toevoegen van instructies na een geheugeninstructie zodat er voldoende pro-
cessorcycli verlopen tussen twee geheugen instructies zodat de eerste instructie altijd al voltooid
is als de tweede instructie het geheugen nodig heeft. De assembler code gegenereerd uit de func-
tie mem1 heeft zo’n gedrag. Het is dus wenselijk dat de compiler altijd machinecode genereert
zoals in 4.4. Maar welke instructies zijn het meest geschikt om toe te voegen?
Figuur 4.8 en 4.9 tonen de verschillende uitvoeringstijden bij het toevoegen van extra instruc-
ties. Eerst heb ik conditionele move (cmov) instructies proberen toe te voegen 1. Als dezelfde
bron en doel operandi worden gebruikt, verandert dit niets aan de toestand van het programma.
Dit blijkt echter niet altijd te werken. Wanneer de conditionele move in een van zijn operandi een
register gebruikt die afhankelijk is van de geheugeninstructie, moet de conditionele move wach-
ten tot deze instructie gedaan is, waardoor de uitvoeringstijd van de conditionele move gewoon1Er is geen speciale reden waarom ik deze instructie eerst heb geprobeerd, dit was gewoon de eerste instructie
die in mij opkwam.
4.3 Implementatie 42
bij het totaal wordt opgeteld en er nog evenveel variatie is. Bijvoorbeeld als in 4.5 het register
ebx gebruikt wordt als een van de operandi. Dit is duidelijk te zien in Figuur 4.8. Er moet
dus gezocht worden naar instructies die niet moeten wachten tot de geheugeninstructie voltooid
is. We kunnen de operandi van de conditionele move veranderen zodat ze niet meer afhankelijk
zijn van de geheugeninstructies. Bijvoorbeeld edx gebruiken als bron- en doeloperand voor de
conditionele move, dan bekomen we de resultaten in Figuur 4.9. Hier is de uitvoeringstijd bij
het toevoegen van conditionele move instructies constant.
De NOP-instructie is een instructie die geen effect heeft op de toestand van het programma
en wordt onder andere gebruikt als padding tussen stukken gealigneerde code. Het voordeel van
deze instructie is dat ze geen operandi heeft. Er kunnen dus geen extra data-afhankelijkheden
ontstaan door het toevoegen van deze instructies. Dit is een groot voordeel ten opzichte van
de conditionele move. Een bijkomend voordeel is dat deze instructie ook weinig processorcycli
gebruikt. Zo kan er veel fijner bepaald worden hoeveel extra cycli er moeten worden toegevoegd
en kan het prestatieverlies zo laag mogelijk gehouden worden. Dit is te zien op zowel Figuur 4.8
als 4.9. Het toevoegen van een NOP-instructie is niet voldoende om de variatie weg te werken.
Het toevoegen van twee of meerdere NOP-instructies na de geheugentoegang werkt het verschil
in uitvoeringstijd volledig weg. Omdat bij het toevoegen van twee NOP-instructies soms nog
wat variatie in uitvoeringstijd kan voorkomen, is het toch beter om altijd drie NOP-instructies
toe te voegen.
Er kan dus besloten worden dat het toevoegen van extra instructies tussen geheugeninstruc-
ties ervoor zorgt dat de tweede instructie nooit meer moet wachten tot de eerst voltooid is. Wat
ervoor zorgt dat de variatie in uitvoeringstijd door afhankelijkheden tussen geheugenadressen
wordt geelimineerd. Er wordt eigenlijk voor gezorgd dat de tweede instructie gewoon altijd moet
wachten door een artificiele wachtijd in te voegen na de eerste instructie. De ideale instructie om
toe te voegen blijkt de NOP-instructie te zijn omdat deze instructie nooit extra data afhankelijk-
heden kan introduceren. Er is ook een minimaal verlies aan prestatie. Omdat de NOP-instructie
weinig cycli gebruikt, kan de artificiele wachttijd min of meer gelijk gekozen worden als de tijd
die de tweede instructie normaal zou moeten wachten als er een afhankelijkheid is gedetecteerd.
4.3 Implementatie
Het is niet mogelijk om de bestaande balanceringsplugin uit te breiden om deze transformatie
uit te voeren. De bestaande plugin werkt op het niveau van de IR (intermediate representati-
4.3 Implementatie 43
Figuur 4.8: Uitvoeringstijden van code in Listing 4.5 bij het toevoegen van instructies na elke ge-
heugeninstructie. Er worden een twee of drie NOP-instructies toegevoegd, dit zijn no-operation
instructies die verder geen invloed hebben op het programma. En een of twee conditionele move
instructies. Hier zijn de operandi van de conditionele move instructie verkeerd gekozen (cmov
%%ebx,%%ebx) zodat ze moeten wachten op de vorige geheugeninstructie. Hierdoor is nog een
vertraging te zien bij offsets 0 en 4. Het kiezen van de toe te voegen instructies mag dus niet
lukraak gebeuren. Bij NOP-instructies komt dit probleem niet voor omdat ze geen argumenten
hebben.
on). Hier worden machineonafhankelijke transformaties uitgevoerd op de code. Het is dus niet
mogelijk om in deze fase transformaties toe te voegen die werken op machine-instructies. Pas
nadat de IR uiteindelijk vertaald is naar machinespecifieke assembler kunnen extra instructies
worden toegevoegd. Er moet dus een pass geschreven worden voor de codegenerator.
Een llvm pass die hiervoor geschikt is, is de MachineFunctionPass. Dit is een pass die
per functie optimalisaties of transformaties kan uitvoeren op niveau van machine-instructies.
De versie van llvm die gebruikt wordt voor de balanceringsplugin is een oudere versie die niet
over dezelfde functionaliteit beschikt als de meest recente uitgave. Het is met deze versie niet
mogelijk om een plugin te implementeren voor een machinefunctionpass. De codegenerator (llc:
llvm system compiler) van de gebruikte llvm versie ondersteunt het laden van passes als plugin
nog niet. Dit heeft vooral te maken met het feit dat het niet eenvoudig via de commandline aan
te duiden is wanneer de pass moet worden uitgevoerd. Er worden namelijk een aantal passes
uitgevoerd tijdens codegeneratie, zoals bijvoorbeeld registerallocatie die heel wat instructies zal
herschrijven om de code te mappen op een eindig aantal registers. Afhankelijk van de volgorde
4.3 Implementatie 44
Figuur 4.9: Uitvoeringstijden van code in Listing 4.5 bij het toevoegen van instructies na el-
ke geheugenoperatie. Hier werden de operandi van de conditionele move-instructie wel goed
gekozen (cmov %%edx,%%edx), zodat de instructie niet hoeft te wachten tot de geheugenin-
structie is voltooid. Toch blijft de NOP-instructie een betere keuze omdat ze voor veel minder
prestatieverlies zorgt.
zou dus een ander resultaat kunnen bekomen worden.
Het is dus nodig om de llvm code zelf aan te passen en er manueel voor te zorgen dat de pass
op het juiste moment wordt uitgevoerd. Gelukkig heeft llvm een tool ”makellvm”waarmee het
mogelijk is om enkel de codegenerator te hercompileren, wat heel wat tijd bespaart. De pass is
geımplementeerd als een architectuurspecifieke pass voor de x86 architectuur. Het is nodig om
dit specifiek voor een bepaalde architectuur te implementeren omdat niet alle doelarchitecturen
de no-operation instructie ondersteunen. Een bijkomend voordeel is dat de pass kan worden
uitgevoerd na registerallocatie. Registerallocatie mapt de virtuele registers op fysische registers.
Omdat er minder fysische registers zijn dan virtuele kan het zijn dat de code hiervoor herschikt
wordt, of extra move instructies worden toegevoegd. Als de transformatie voor registerallocatie
zou worden uitgevoerd kan het zijn dat deze teniet worden gedaan door registerallocatie.
Ik heb een aantal vrij eenvoudige algoritmes geımplementeerd in de pass. Zoals eerder
vermeld wordt een conditionele move-instructie toegevoegd voor elke geheugenoperatie, om ofwel
het echte of het dummy geheugenadres te gebruiken. Het algoritme gaat op zoek naar het patroon
van een conditionele move-instructie gevolgd door een instructie waarvan een van de operandi
het geheugen aanspreekt. Als zo’n patroon gevonden is, worden de nodige machine-instructies
toegevoegd. En dit voor alle functies uit het programma.
4.3 Implementatie 45
Het is nu natuurlijk mogelijk dat er een aantal vals positieve matches gebeuren. Dit heeft
wel geen invloed op de correctheid van het programma, maar eventueel wel op de prestatie van
de code. Bij encryptietools of programma’s is de meest rekenintensieve code het encryptiealgo-
ritme zelf. Deze code wordt normaalgezien altijd gebalanceerd. Het toevoegen van enkele extra
instructies buiten deze code, dus in code die niet gebalanceerd moet worden, zal weinig invloed
hebben op de totale prestatie van het programma. Het is mogelijk om de annotaties van de
functie te controleren. Dit is echter niet geımplementeerd.
Invoegen van NOP-instructies is ook niet ideaal. Omdat deze instructie geen operandi heeft
kan de scheduler deze instructie plaatsen waar hij wil. Het is dus niet zeker dat de toegevoegde
NOP-instructies worden uitgevoerd op de plaats waar ze in de code staan. Dit kan het random
gedrag verklaren van de uitvoeringstijd bij het toevoegen van NOP-instructies. Bijvoorbeeld het
toevoegen van x NOP-instructies geeft een constante uitvoeringstijd voor een gegeven stuk code,
maar het toevoegen van x+1 instructies dan weer niet. Toch is het zoeken van het juiste aantal
instructies per geval eenvoudiger dan het zoeken van gepaste registers voor unaire of binaire
instructies.
algoritme 1
Mijn eerste poging om te automatiseren is een zeer eenvoudig en naıef algoritme. De machine-
instructies van alle functies worden overlopen tot een conditionele move-instructie is gevonden.
Daarna wordt gecontroleerd als de daaropvolgende instructie een geheugenoperand heeft. In-
dien dit zo is wordt na deze instructie een aantal NOP-instructies ingevoegd. Dit is geen goede
aanpak omdat in de gegenereerde code niet altijd evenveel instructies tussen twee geheugenin-
structies staan. Drie NOP instucties toevoegen bij het ene stuk code zal bijvoorbeeld dan niet
meer werken bij een ander stuk code. Ook is het niet altijd nodig om na elke geheugeninstructie
NOP-instructies toe te voegen omdat deze niet altijd worden gevolgd door een andere geheugen-
instructie. Dit is aangetoond in Figuur 4.10. Een vast aantal NOP-instructies toevoegen werkt
dus nooit in alle gevallen.
algoritme 2
Een betere aanpak is om ervoor te zorgen dat er altijd een minimum aantal instructies tussen
twee geheugeninstructies worden uitgevoerd. Het algoritme telt hoeveel instructies er tussen
twee geheugeninstructies liggen en beslist op basis daarvan hoeveel extra instructies er moeten
4.3 Implementatie 46
Figuur 4.10: Algoritme 1. Uitvoeringstijden na het toevoegen van een constant aantal NOP-
instructies na het balanceren van de code in Listing 4.1 (mem3) en in 4.2 (mem1). Het toevoegen
van een constant aantal NOP-instructies is duidelijk geen goede aanpak.
worden toegevoegd. Een bijkomend voordeel van dit algoritme is de prestatiewinst omdat niet na
elke geheugeninstructie extra NOP-instructies worden toegevoegd. Figuren 4.11 en 4.12 tonen
de resultaten van dit algoritme.
Minimum drie of vier instructies tussen twee geheugeninstructies werkt niet in beide gevallen.
Minimum vijf instructies werkt wel in beide gevallen. Het verschil tussen de twee gevallen kan
verklaard worden door het feit dat NOP-instructies geen afhankelijkheden hebben en in het ene
geval waarschijnlijk in een andere volgorde worden gescheduled. Vijf instructies werkt wel in
beide gevallen, maar het prestatieverlies is bijna niet meer toelaatbaar, vooral omdat maar een
kleine variatie in uitvoeringstijd wordt weggewerkt.
algoritme 3
Dit algoritme is een aangepaste versie van algoritme 2. Het zorgt in beide gevallen voor de laagste
constante uitvoeringstijd. Bij twee geheugeninstructies waar minder dan drie andere instructies
tussen staan wordt het aantal instructies ertussen aangevuld tot 4. Met andere woorden als er
reeds drie of meer instructies tussen twee geheugeninstructies staan wordt er niets aangepast.
Anders wordt ervoor gezorgd dat er minimum vier instructies tussen staan. Dit combineert de
beste resultaten van de twee gevallen uit het vorige algoritme. Uitvoeringstijden van de stukjes
code zijn te zien op Figuur 4.13.
4.4 Resultaten 47
Figuur 4.11: Algoritme 2. Uitvoeringstijden nadat ervoor gezorgd is dat er een minimum aantal
instructies tussen twee geheugeninstructies worden geplaatst voor Listing 4.1 (mem3).
4.4 Resultaten
Algoritme 3 elimineert het resterende verschil in uitvoeringstijd in beide gevallen die in dit hoofd-
stuk zijn getest (Figuur 4.13). De gemiddelde uitvoeringstijden van de twee gevallen (mem3:
Listing 4.1 en mem1: Listing 4.2) zijn te zien in Figuur 4.14. Omdat bij mem1 geen extra
instructies wordt bijgevoegd is de uitvoeringstijd na balancering en na eliminatie van load by-
passing gelijk. Na eliminatie van load bypassing in de code van mem3 is de code even snel als
die van mem1. Er is dus geen groot prestatieverlies door eliminatie van load bypassing.
Deze algoritmes zijn enkel getest op de twee artificiele voorbeeldgevallen. Toch is dit repre-
sentatief voor andere crytografische code. Hier wordt enkel een store- en een loadinstructie in
een loop uitgevoerd. In encryptiealgoritmes wordt er nog veel andere code uitgevoerd en zal
het effect van load bypassing niet zo duidelijk zijn als in de gevallen die in dit hoofdstuk staan
beschreven. Met andere woorden, als er hier geen verschil meer te merken is zal het verschil in
cryptografische algoritmes nog veel kleiner zijn.
De algoritmes die in dit hoofdstuk zijn beschreven hebben slechts beperkte functionaliteit.
Omdat de algoritmes in dit hoofdstuk uitgevoerd worden op een stukje code met enkel een store
en load instructie wordt niet gecontroleerd als de gevonden instructie een load of store is. Er
wordt enkel gecontroleerd of een van de argumenten het geheugen gebruikt. Dit is voldoende voor
de twee testgevallen. Om de algoritmes uit te voeren op echte broncode moeten ze uitgebreid
worden om enkel store-instructies gevolgd door loadinstructies aan te passen.
4.4 Resultaten 48
Figuur 4.12: Algoritme 2. Uitvoeringstijden nadat ervoor gezorgd is dat er een minimum aantal
instructies tussen twee geheugeninstructies worden geplaatst voor Listing 4.2 (mem1).
Figuur 4.13: Algoritme 3. Als er minder dan drie instructies tussen twee geheugeninstructies
staan wordt dit aangevuld tot vier door NOP-instructies toe te voegen. Anders wordt de code
niet aangepast. Dit geeft voor beide codefragmenten het beste resultaat.
Figuur 4.14: Uitvoeringstijden van algoritme 3 toegepast op de twee voorbeeldgevallen.
CACHEGEDRAG 49
Hoofdstuk 5
Cachegedrag
In dit hoofdstuk wordt de invloed van de cache op de uitvoeringstijd bestudeerd. Eerst worden
de gevolgen van if-conversie op het cachegedrag besproken. Daarna wordt een specifiek geval
bestudeerd en een mogelijke oplossing voorgesteld.
5.1 Cachegeheugen
Een cache is een buffer tussen de processor en het hoofdgeheugen. Het houdt de meest gebruikte
data bij zodat niet telkens het hoofdgeheugen moet geraadpleegd worden. Cachegeheugen is veel
sneller dan het hoofdgeheugen. Op moderne architecturen wordt een hierarchische cachestruc-
tuur gebruikt. Dit wil zeggen dat er verschillende niveaus van cachegeheugens aanwezig zijn met
telkens een grotere toegangstijd. Op Figuur 5.1 worden de verschillende cachegeheugens en hun
toegangstijden schematisch voorgesteld. Als een programma gegevens uit het geheugen nodig
heeft, zal eerst worden gecontroleerd als de data aanwezig is in het eerste cacheniveau. Indien
niet (cache-miss) zal een van de volgende niveaus en uiteindelijk het hoofdgeheugen worden
aangesproken. Naargelang de gevraagde data aanwezig is in het cachegeheugen zal een load of
store-instructie dus een andere uitvoeringstijd hebben.
5.2 Invloed van cachegeheugen op uitvoeringstijd
Zoals in de inleiding en het vorige hoofdstuk reeds vermeld is, worden load- en store-instructies
uitgevoerd op dummy geheugenadressen als ze worden opgeroepen in een tak die niet wordt
uitgevoerd. Afhankelijk van de invoer van het programma kan dus ofwel het originele geheuge-
nadres ofwel het dummy geheugenadres worden gebruikt. Dit kan voor een ander cachegedrag
5.2 Invloed van cachegeheugen op uitvoeringstijd 50
Figuur 5.1: Cachelayout van een Intel Core2 Duo processor. Het RAM geheugen is geen deel
van de processor en wordt via de Northbridge aangesproken.
zorgen. De gegevens op het originele geheugenadres kunnen zich reeds in de cache bevinden en
de gegevens op het dummy geheuegenadres niet. Als de instructie wordt uitgevoerd in een tak
die niet genomen is, zal een cache-miss optreden. Bijgevolg moet het volgende cacheniveau ge-
raadpleegd worden, wat voor een extra vertraging zorgt. Op deze manier kan het cachegedrag de
totale uitvoeringstijd van een programma beınvloeden. If-conversie introduceert dus een nieuwe
data-afhankelijkheid.
Er kunnen twee hoofdgevallen onderscheiden worden. Deze worden getoond op figuren 5.2
en 5.4. In het eerste geval (Figuur 5.2) is het geheugenadres altijd gekend. In dit geval is het
mogelijk om een dummy geheugenadres te berekenen aan de hand van het gekende adres. Als
a een geldig adres is, is het zelfs mogelijk om de load instructies het originele geheugenadres te
laten gebruiken.
In het tweede geval (Figuur 5.4) is het adres niet gekend wanneer de tak niet wordt genomen.
A is dan een nullpointer en het is niet mogelijk om aan de hand hiervan een dummy adres te
berekenen.
Figuur 5.2: Geval waar het originele geheugenadres gekend is. Het is mogelijk om het dummy
geheugenadres te kiezen als functie van het originele.
Er moet dus gezocht worden naar een manier om de dummy geheugenadressen zo te kie-
zen dat de variatie in uitvoeringstijd van een programma ten gevolge van het cachegedrag zo
5.3 Bestudeerde oplossingen 51
Figuur 5.3: Geval waar het originele geheugenadres niet gekend is. Er moet een andere manier
gezocht worden om een geschikt dummy geheugenadres te kiezen.
klein mogelijk is. We zeggen zo klein mogelijk omdat het bijna onmogelijk is om het originele
geheugenadres en het dummy geheugenadres in dezelfde toestand te hebben (Ofwel beide in de
cache ofwel alletwee enkel in geheugen). De cache wordt beınvloed door externe factoren zoals
andere draden waarover we geen controle hebben. Het is dan ook niet triviaal om een algeme-
ne oplossing te vinden die direct alle variatie ten gevolge van cache gedrag uit een programma
haalt. Het is echter wel nuttig om te kijken als een oplossing kan gevonden worden voor bepaalde
codeconstructies die voor variatie in uitvoeringstijd zorgen ten gevolge van het cache gedrag.
5.3 Bestudeerde oplossingen
Het stukje code in Listing 5.1 is een voorbeeld van een codeconstructie die ervoor kan zorgen dat
het cachegedrag extra variatie toevoegt aan de totale uitvoeringstijd van het programma. De
code bestaat uit een lus met daarin twee andere lussen. De eerste lus leest 8000 gehele getallen
uit een array. De Intel Core2 Duo processor heeft 32KByte L1 cache geheugen, de cache wordt
dus volledig overschreven met gegevens van de eerste loop. Daarna worden afhankelijk van de
waarde van a, 8000 andere getallen uit het geheugen gelezen. Als a waar is wordt de cache
overschreven met gegevens uit de tweede loop en zullen in de volgende iteratie cache-misses
optreden in de eerste loop. Als a vals is worden er geen gegevens uit het geheugen gelezen en
zullen de gegevens van de eerste loop voor het grootste deel nog in de cache zitten.
5.3 Bestudeerde oplossingen 52
Listing 5.1: C-code die variatie in uitvoeringstijd ten gevolge van cache gedrag aantoont.
for(i=1;i <500000;i++){
for(k=0;k <8000;k++){
result += *(mem+k);
}
for(j=0;j <8000;j++){
if(a){
result += *(mem +10000+j);
}
}
}
Na balancering verandert er niet veel aan dit gedrag. Zoals if-conversie nu is geımplementeerd,
wordt per geheugeninstructie een dummy geheugenlocatie gealloceerd op de stack. Als a vals is
zal dus 8000 keer hetzelfde geheugenadres worden opgevraagd. Ook nu zullen de meeste gegevens
uit de eerste lus nog in de cache aanwezig zijn. Afhankelijk van de waarde van a treden dus meer
cache-misses op in de eerste loop en dit zou kunnen bijdragen tot variatie in de uitvoeringstijd
van het programma.
Dit kan opgelost worden door in plaats van per geheugeninstructie een dummy geheugen-
locatie op de stack te alloceren een blok geheugen te alloceren voor het hele programma. Het
dummy geheugenadres kan dan berekend worden door de laatste x bits van het originele ge-
heugenadres te gebruiken als index binnen het gealloceerde blok geheugen. Op deze manier
wordt bij een instructie die naar een constant origineel adres schrijft ook naar een constant
dummyadres geschreven. Als de instructie uit een array leest zal dan ook telkens uit een ander
dummy geheugenlocatie worden gelezen. In de voorbeeldcode zal loop twee nu altijd de cache
overschrijven met zijn gegevens, zelfs als a vals is.
De grootte van het gealloceerde blok is gelijk aan de grootte van de cache. Een blok kleiner
dan de cache zal de variatie in uitvoeringstijd wel verkleinen, maar het zal niet mogelijk zijn
om in de tweede loop de volledige cache te overschrijven. Het aantal bits x moet exact gelijk
zijn aan het binair logaritme van het gealloceerde blok geheugen. Als het kleiner is zal niet het
volledige geheugenblok gebruikt worden. Als het groter is kan buiten het geheugenblok worden
geschreven wat een segmentation fault kan geven.
Er is ook geen probleem wanneer het adres niet gekend is of als het om een nullpointer gaat.
5.4 Implementatie 53
De laaste bits van het originele adres zijn dan 0 of een arbitraire waarde. In het geval dat de
bits 0 zijn, zal het dummyadres wijzen naar het begin van het gealloceerde blok geheugen. Dit is
gelijk aan de manier waarop in de huidige plugin een waarde wordt gealloceerd. Een arbitraire
waarde zal eveneens naar een random index binnen het geheugenblok wijzen.
5.4 Implementatie
Deze transformatie is geımplementeerd als een uitbreiding op de bestaande balanceringsplugin.
De code die dummy geheugenadressen alloceert heb ik vervangen door het algoritme dat in de
vorige sectie beschreven is. Het gebruiken van het originele adres als index in een gealloceerd blok
geheugen is zeer eenvoudig. Eerst wordt een logische and uitgevoerd van het originele adres en
een bitmasker die de meest significante bits op nul zet. Enkel voldoende bit worden behouden om
het gealloceerde blok geheugen te indexeren. Dit resultaat wordt dan opgeteld bij het beginadres
van het blok geheugen. In formulevorm: DummyAdres = ADD(GeheugenBlokAdresPointer ,
AND(OrigineelAdres, 1 . . . 11).
5.5 Resultaten
Het stuk C-code in Listing 5.1 wordt uitgevoerd met twee invoersets. In de eerste set is a
constant vals en wordt de conditionele code nooit uitgevoerd. In de tweede set is a constant
waar en wordt de conditionele code altijd uitgevoerd. Figuur 5.4 toont de resultaten van de testen
uitgemiddeld over 40 uitvoeringen. Bij de originele manier van geheugenadressen alloceren is er
een duidelijk verschil tussen de gemiddelde uitvoeringstijden. De p-waarden van de t-test zijn
dus ook erg laag, wat er op duidt dat er een duidelijk verschil is tussen de twee samples van
uitvoeringstijden. Bij de nieuwe methode liggen de gemiddelde uitvoeringstijden veel dichter
bij elkaar. Uit de hoge p-waarde kan afgeleid worden dat er geen merkbaar verschil meer is
tussen de uitvoeringstijden van de twee invoersets. De laatse rij toont het aantal cache-misses
per geval voor de oude en nieuwe manier van alloceren. Waar er bij de oude manier een duidelijk
verschil was zijn er nu in beide gevallen evenveel cachemisses. Omdat er code wordt toegevoegd
die voor elke geheugeninstructie het correcte dummy geheugenadres moet berekenen, is er een
prestatieverlies van 14,5%.
BESLUIT EN TOEKOMSTPERSPECTIEVEN 55
Hoofdstuk 6
Besluit en toekomstperspectieven
Binnen de vakgroep zijn reeds compilertransformaties ontworpen die het controleverloop onaf-
hankelijk maakt van de invoer van het programma. Mijn thesis vult dit onderzoek aan met een
aantal tranformaties die data-afhankelijkheden elimineren.
In de eerste plaats wordt een alternatieve oplossing voorgesteld om de variabele uitvoerings-
tijd van de gehele deling te elimineren. Door het parallel uitvoeren van instructies met een
langere vaste uitvoeringstijd met de originele deling kan de globale uitvoeringstijd onafhankelijk
gemaakt worden van de uitvoeringstijd van de deling. Deze transformatie is veel performanter
dan de reeds bestaande transformatie[2] bij het uitvoeren van een modulair exponentiatie algo-
ritme: 109% en 78% trager voor respectievelijk 32- en 64bit code. Reeds bestaande technieken
zijn 2300% en 1439% trager voor respectievelijk 32- en 64bit code. Dit is een enorme snelheids-
winst. Een nadeel ten opzichte van de bestaande techniek is dat de code nog steeds kwetsbaar
is voor een resource contention aanval.
Vervolgens werd het pijplijngedrag van load- en store-instructies onderzocht. Naargelang
er afhankelijkheden worden gedetecteerd tussen een opeenvolgende store en loadinstructie kan
load bypassing optreden. Dit kan zorgen voor variabele uitvoeringstijd van een programma.
Na balancering is de meeste variatie reeds geelimineerd. In deze paper wordt een oplossing
voorgesteld die de resterende variatie elimineert. Er wordt een artificiele vertraging toegevoegd
tussen load- en store-instructies door het toevoegen van NOP-instructies. Op deze manier wordt
er geen load bypassing meer uitgevoerd. Op de geteste gevallen geeft het elimineren van load
bypassing een prestatieverlies van 0% en 14% afhankelijk van de broncode.
Ten laatste werd nog een geval onderzocht waar het cachegedrag invloed heeft op de uitvoe-
ringstijd van een programma. Door het implementeren van een nieuwe manier van alloceren van
BESLUIT EN TOEKOMSTPERSPECTIEVEN 56
dummy geheugenadressen bij if-conversie wordt deze variatie weggewerkt.
Combinatie van reeds bestaande technieken gecombineerd met de oplossingen hier voorge-
steld zorgen ervoor dat nevenkanalen zoals sprongvoorspeller en instructiecache volledig zijn
afgesloten. De variatie in uitvoeringstijd wordt door de transformaties in deze paper nog verder
gereduceerd, waardoor hier veel minder informatie uit kan gehaald worden. Reeds bestaande
transformaties gebaseerd op if-conversie tesamen met de tranformaties beschreven in deze paper
zullen het een aanvaller aanzienlijk lastiger maken om geheime informatie te achterhalen door
middel van micro-architecturale crypto-analyse.
Verder onderzoek kan nog gebeuren om de performantie van de transformaties te optimalise-
ren en andere gevallen te analyseren waar de cache nog invloed kan hebben op de uitvoeringstijd.
Het is ook nuttig om mogelijkheden te onderzoeken om de data-cache als nevenkanaal af te slui-
ten.
DETAILED INFORMATION 57
Bijlage A
Detailed information
A.1 CPU Details
processor : 0
vendor_id : GenuineIntel
cpu family : 6
model : 23
model name : Intel(R) Core(TM)2 Duo CPU T6400 @ 2.00GHz
stepping : 10
cpu MHz : 1200.000
cache size : 2048 KB
physical id : 0
siblings : 2
core id : 0
cpu cores : 2
apicid : 0
initial apicid : 0
fdiv_bug : no
hlt_bug : no
f00f_bug : no
coma_bug : no
fpu : yes
fpu_exception : yes
A.1 CPU Details 58
cpuid level : 13
wp : yes
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat
pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe nx lm
constant_tsc arch_perfmon pebs bts pni monitor ds_cpl est tm2 ssse3
cx16 xtpr sse4_1 lahf_lm
bogomips : 3989.96
clflush size : 64
power management:
processor : 1
vendor_id : GenuineIntel
cpu family : 6
model : 23
model name : Intel(R) Core(TM)2 Duo CPU T6400 @ 2.00GHz
stepping : 10
cpu MHz : 1200.000
cache size : 2048 KB
physical id : 0
siblings : 2
core id : 1
cpu cores : 2
apicid : 1
initial apicid : 1
fdiv_bug : no
hlt_bug : no
f00f_bug : no
coma_bug : no
fpu : yes
fpu_exception : yes
cpuid level : 13
wp : yes
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat
A.2 Cache Details 59
pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe nx lm
constant_tsc arch_perfmon pebs bts pni monitor ds_cpl est tm2 ssse3
cx16 xtpr sse4_1 lahf_lm
bogomips : 3989.96
clflush size : 64
power management:
A.2 Cache Details
/sys/devices/system/cpu/cpu1/cache/index0
Level: 1
Type: Data
Size: 32K
Number of sets: 64
/sys/devices/system/cpu/cpu1/cache/index1
Level: 1
Type: Instruction
Size: 32K
Number of sets: 64
/sys/devices/system/cpu/cpu1/cache/index2
Level: 2
Type: Unified
Size: 2048K
Number of sets: 4096
BIBLIOGRAFIE 60
Bibliografie
[1] Simcha Gochman, Avi Mendelson, Alon Naveh, Efraim Rotem “Introduction to Intel Core
Duo Processor Architecture”
[2] Bart Coppens , Ingrid Verbauwhede, Koen De Bosschere, Bjorn De Sutter “Practical
Mitigations for Timing-Based Side-Channel Attacks on Modern x86 Processors”
[3] Jack Doweck “Inside Intel Core Microarchitecture and Smart Memory Access” “An In-
Depth Look at Intel Innovations for Accelerating Execution of Memory-Related Instruc-
tions”
[4] Chris Lattner “Introduction to the LLVM Compiler Infrastructure”
[5] The LLVM Compiler Infrastructure http://llvm.org/
[6] Onur Onur Acıimez, Jean-Pierre Seifert,etin Kaya Ko “Micro-architectural cryptanalysis”
[7] D. Molnar, M. Piotrowski, D. Schultz, and D. Wagner, The program counter security
model: Automatic detection and removal of control-flow side channel attacks, pp. 156168,
2005.
[8] Paul C. Kocher; “Timing Attacks on Implementations of Diffie-Hellman, RSA, DSS, and
Other Systems”
[9] Shay Gueron, “Advanced encryption standard (AES) Instruction set”
[10] Fisher, J.A. and Faraboschi, P. and Young, C., “Embedded Computing, A VLIW Approach
to Architecture, Compilers and Tools”
[11] by Joseph A. Fisher, Paolo Faraboschi, Cliff Young, “Embedded Computing: A VLIW
Approach to Architecture, Compilers and Tools”
BIBLIOGRAFIE 61
[12] David I. August and John W. Sias and Jean-Michel Puiatti and Scott A. Mahlke and
Daniel A. Connors and Kevin M. Crozier and Wen-mei W. Hwu, “The program decision
logic approach to predicated execution”
[13] J. Coke, H. Balig, N. Cooray, E. Gamsaragan, P. Smith, K. Yoon, J. Abel, and A. Valles,
“Improvements in the Intel Core 2 processor family architecture and microarchitecture”,
Intel Technology Journal, vol. 12, no. 03, pp. 179-192, 2008.
[14] William Stallings “Cryptography and Network Security, Principles and Practices”
[15] D. Brumley, D. Bonch, “Remote Timing Attacks are Practical.”
[16] K. Gandolfi, C Mourtel, F. Olivier, “Electromagnetic Analysis: Concrete Results.”
[17] Onur Aciicmez, Cetin Kaya Koc, Jean-Pierre Seifert, “On the power of simple branch
prediction analysis.”
[18] J. Shen and M. Lipasti “Modern Processor Design: Fundamentals of Superscalar Proces-
sors” McGraw-Hill, 2005