운영체제 & 컴퓨터구조

[ARCH]#Cache Memory, Meltdown & Spectre

코딩하는상후니 2023. 3. 9. 11:51

 

 

 


 

지난 단락에서 CPU 내부 구성을 이루는 ALU, Control Unit, Register 에 대해서 알아보았다.
이번 단락에서는 캐시 메모리와 CPU 의 특성, 그에 따라 발생할 수 있는 위험을 알아보려한다.
먼저 캐시 메모리에 대해서 알아보자.
 
 
 
 

* 캐시 메모리 ( Cache )

 

 

 

캐시 메모리는 보통 RAM 보다 CPU 가까운 곳에 위치한다.
RAM 이 기억장치로서의 역할을 하고 있는데 굳이 옆에 따로 캐시 메모리를 놓아야하는 이유는 무엇일까 ??
 
RAM 에서 데이터를 가져오는 속도가 CPU 클럭를 못 따라가기 때문이다.
그렇다면 RAM 에서 데이터를 가져오는 속도를 높일 필요가 있는데 이는 비용과 다른 문제를 야기한다.
일반적으로 RAM 내부에서 bit 를 저장하는데 사용되는 방식은 DRAM 방식이다.
 
간단하게 DRAM 은 축전기를 사용하는 방식인데 아주 작은 크기로 대용량을 만들 수 있지만
문제는 트랜지스터를 이용해서 1 을 저장했을 때 시간이 지나면 방전되어서 결과값이 휘발될 수 있다.
때문에 지속적으로 전류를 공급해주어야한다.
( 결과값이 동적이라는 의미로 Dynamic Access Memory 이라 부른다. )
 
이와 다른 방식인 SRAM 은 많은 트랜지스터를 이용해 1 bit 저장을 유효시킬 수 있지만
용량이 크고 트랜지스터를 많이 사용하는 만큼 비싸다는 특징이 있다.
이전에 레지스터 단원에서 살펴본 Flip-Flop 개념이 이에 사용된다.
( 결과값이 고정되어있다는 의미에서 Static Access Memory 용어를 사용한다. )
 
RAM 에서 데이터를 가져오는 속도를 높이기 위해서 다른 방식 ( SRAM ) 을 사용하면 대용량의 이점이 사라지며
비용도 비싸지는 또 다른 문제가 발생한다.
이를 해결하기 위해서 캐시 메모리라는 또 다른 메모리 영역을 만든 것이고
RAM 에 비해서 상대적으로 작을 수 밖에 없으며 비용이 비싼 대신 빠르다.
 
또한,
캐시 메모리의 종류는 다양하고 각각 L1, L2, L3 ... 등으로 나뉘어진다.
L1 종류의 캐시 메모리는 작고 굉장히 빠른 소자들로 구성되며 단계가 낮아질수록 느리지만 용량이 크다.
 
 
좀 더 구체적으로 캐시 메모리 특성에 대해서 몇 가지 살펴보자.
 
 
 

* 데이터 선별 기준

 
지금까지 설명한 캐시 메모리의 역할은 CPU 클럭 속도를 높이기 위해서 RAM 에 접근해서 데이터를 읽을 때,
다음에 쓸 데이터를 미리 가져오는 방식이다. 그렇다면 어떤 기준으로 데이터를 가져올까 ??
 
 
* 시간 지역성
=> 이전에 사용된 데이터를 다시 쓸 확률이 높다라는 전재로 최근에 사용된 데이터의 경우를 고려한다.
ex) for, while 문에 사용된 변수
 
 
* 공간 지역성
=> 해당 데이터가 있는 메모리 주변의 데이터들이 사용될 가능성을 고려한다.
ex) 연속된 배열
 
 
* 순차적 지역성
=> 메모리에 저장된 명령어 순서를 기준으로 다음에 수행되는 명령어들을 고려한다.
ex) 분기 ( brench )
 
 
위 기준들을 적절히 조합해서 캐시 메모리가 데이터를 가져올 수 있는 기준이 정해지고
CPU 는 RAM 에 접근하기 이전에 자신에게 가까운 캐시 메모리부터 훑으면서 데이터가 존재하는지 확인한다.
이 때,
CPU 가 캐시 메모리에게서 원하는 데이터를 얻었을 때, '캐시 히트 ( Cache Hit )'되었다고 말하며 이를 적중률이라고 표현한다.
반대로 데이터를 찾지 못해 RAM 까지 가게 되는 상황을 '캐시 미스 ( Cache Miss )' 라고 부른다.
 
 
 

*  Cache Hit / Miss

 
당연한 이야기지만, 캐시 적중률이 높다는 것은 캐시 메모리의 효율이 높다는 것이다.
상대적으로 낮은 정도의 수치에서 높은 수치를 올리는 것은 높은 수치에서 더 높은 수치로 만드는 일보다 쉽다.
예를 들어, 0 -> 50% 로 가는 것보다 98% -> 99% 를 올리는 것이 더 어렵다.
 
그렇다면 궁극적으로 캐시 Hit 율이 100% 가 될 수 있을까 ??
 
그럴 순 없다.
왜냐하면 어찌됐던 프로그램이 처음 실행될 땐, RAM 에 저장된 명령어와 데이터들을 가져와야하기 때문이다.
때문에 우리는 최대한 캐시 Hit 율을 높이는 방향을 지향해야한다.
이를 위해 우리는 캐시 미스가 날 수 있는 경우의 수는 무엇인지 알아야한다.
 
 
* Compulsory miss ( Cold miss )
com ( 완전히 ) + puls ( 몰아가다 ) 이 합쳐져서 Compulsory ( 강제적인 ) 의미를 가진 해당 miss 는
우리가 앞서본 프로그램 첫 실행처럼 어쩔 수 없이 발생할 수 밖에 없는 미스를 말한다.
 
 
* Conflict miss
Con, Com ( 함께 ) + flict ( 치고 받다 ) 이 합쳐져서 Conflict ( 서로 다투다 ) 의미를 가진 해당 miss 는
RAM 에 비해 상대적으로 작은 캐시 메모리는 어쩔 수 없이 중복되는 부분이 생길 수 있다.
RAM 의 일정 영역이 해당 캐시 메모리의 일정 영역에 사상된다는 가정에서
해당 캐시 메모리에 저장해놓은 데이터가 다른 데이터로 덮어쓰여지는 상황을 말한다.
이 후 설명할 캐시 저장 방식과 관련이 있다.
 
 
* Capacity miss
Cap ( 잡다 ) + ac ( 공간 ) 이 합쳐져서 Capacity ( 수용량 ) 의미를 가진 해당 miss 는
캐시 메모리의 전체 영역이 사용 중일 때 발생하는 미스이다.
이에 따라, 크기가 크고 조금 느린 캐시 메모리에 인계되거나 해당 캐시 메모리에 덮여쓰여짐으로써
기존의 데이터, 현재 쓰여질 데이터 어느 부분에서든 캐시 미스가 발생한다.
 
 
첫번째 살펴본 강제적인 미스 ( Compulsory miss ) 의 경우는 어쩔 수 없으니  논외로 하고
서로의 주소를 참조하는 미스, 수용량 문제에 따른 미스 이 두 가지 경우가 중요해보인다.
 
하지만,
수용량 문제에 관한 Miss ( Capacity miss ) 의 경우, 크기를 늘리기 위해선 비용이 들어가며 부피가 커지며
속도도 느려질 수 있으며 그만큼 전력을 많이 소비한다.
 
궁극적으로, 소프트웨어적으로 우리가 할 수 있는 것은
2번째 설명한 미스 ( Conflict miss ) 를 최소화하는 방향으로 진행시켜야하겠다.
이를 위해 가져온 데이터를 캐시에 어떻게 효과적으로 저장하느냐가 중요한 문제이다.
 
 
 
 

*  캐시 메모리 저장 알고리즘

 
어떻게 저장할지 간단하게 우리는 2가지 정도 생각해볼 수 있다.
첫번째는 RAM 영역을 전담하는 캐시 메모리 영역을 부여하는 것이다.
두번째는 가장 오래된 캐시 메모리 안 데이터를 최근 것으로 덮어씌우는 일이다.
 
전자는 해당 영역이 다른 데이터로 덮여씌여지는 문제가 발생하고
후자는 해당 영역의 데이터가 어디 있는지 찾는 비용이 발생한다.
 
이러한 캐시 저장 방식을 캐시 배치 정책 ( Cache Placement Policy ) 라고 하며
각각의 방식을 Direct Mapped Cache, Fully Associative Cache 라고 한다.
 
 
* Direct Mapped Cache
앞서 설명한 전자에 해당한다.
캐시 메모리 영역 일부가 RAM 영역의 일부를 담당하며 해당 RAM 영역의 데이터가 필요하거나 쓰여질 것으로 예측될 때,
해당 영역에 데이터를 저장하는데 문제는 앞서 보았듯 Conflict miss 가 발생한다는 것이다.
왜냐하면 기존의 데이터를 덮어씌우기 때문이다.
 
 
 
* Fully Associative Cache
앞서 설명한 후자에 해당한다.
캐시 메모리 처음부터 끝까지 차곡차곡 데이터가 쌓여지기 때문에 공간적으로는 알뜰하게 쓸 수 있다.
다만 어떤 규칙이 없기 때문에 데이터를 찾기 위해 완전 탐색이 필요하다.
굉장히 비효율적으로 보이지만 이를 위해 CAM ( Content Addressable Memory ) 라는
특수한 메모리 구조 형태를 고안해서 TLB 에 쓰여진다고 한다. 가격이 비싸다고 한다.
 
 
그렇다면 오늘날 우리가 쓰이는 캐시 메모리 저장 방식은 무엇일까 ??
위 두 가지 방식이 혼합되어진 Set Associative Cache 방식이 그것이다.
모든 것에는 일장일단이 존재하며 더 나은 방향을 추구하기 위해 서로 보완해 균형을 이루게 하는
'중용' 의 지혜가 여기서도 보인다.
 
 
* Set Associative Cache
위 두 가지 방식을 섞은 방식이다.
간략하게 설명하면 캐시 메모리를 행렬로 구분할 때,
어떤 RAM 영역을 하나의 행으로 구분하고 비워진 열에 데이터를 채워넣는 방식이다.
영역을 행으로 구분하기 때문에 데이터를 찾을 수 있는 규칙이 있고 완전 탐색까지는 아닌 해당 행만을 탐색하면 되기 때문에
적절하게 두 가지 방식을 섞었다고 볼 수 있다.
 
 
 
 
 
 
 
이전에 CPU 단원에서 다루지 못한 CPU 의 특성
비순차적 실행 ( Out-of-Order Execution ) 와 분기 예측 ( Branch Prediction ) 을 알아보려고 한다.
순서상 캐시 메모리 앞에 놓으려했지만 다시 상기하는 차원에서 보면 좋을 것 같다.
또, 이 특성과 캐시 메모리의 역할로 인해 발생하는 Meltdown, Spectre 문제가 존재하기 때문이다.
 
 

* 비순차적 실행 ( Out-of-Order Execution )

 
 
비순차적 실행은 CPU 가 명령어 순서대로 처리하지 않고 가능한 빠르게 처리하기 위해서
명령어의 순서를 변경해 처리하는 방식이다.
이는 CPU 가 한번의 사이클을 돌 때, ( Clock )
처리할 수 있는 명령어의 양이 늘어남으로써 더욱 효율적으로 동작하게 만들 수 있는 여지가 생겨났음을 의미한다.
이 명령어의 개수는 제조사마다 다르며 이를 나타내는 지표를 'IPC ( Instruction Per Cycle )' 이라고 한다.
 
프로그램은 일련의 작성된 명령어들로 구성되어있으며 CPU 는 해당 명령어를 받아 처리한다.
이 때, CPU 판단 하에 명령어 순서와 다르게 효율적으로 처리할 수 있다면 다른 순서로 배치한다.
이 기술을  '코드 재배치 ( Code Reordering )' 이라고도 한다.

 

 

 

그림을 참고하자.
CPU 가 총 처리할 수 있는 명령어 갯수가 5 개라고 가정할 때, 1-1, 1-2, 2-1, 2-2, 3 과정의 명령어가 들어온다.
1-2 작업은 1-1 이 완료되고 나서 수행되는 작업으로써 선행 작업인 1-1 작업 완료가 필요하다.
이런 과정을 종속이 포함된 과정으로 설명하는데 비순차적 실행 방식은
종속성이 없는 명령어들을 먼저 실행하여 처리 속도를 높이는 방식이다.
 
비순차적 실행은 대부분의 경우 성능을 향상시키지만, 명령어 실행 순서가 변경됨으로써 발생하는 부작용을 처리해야 하므로
일부 상황에서는 순차적 실행보다 더 느릴 수 있다.
또한, 명령어 간 종속성이 많은 경우에는 비순차적 실행이 큰 성능 향상을 가져오지 못할 수 있다.
 
 

* 분기 예측 ( Branch Prediction )

 
 
분기 예측은 간단하게 분기되어 해당 조건에 실행될 부분을 미리 연산해놓는 것이다.
이런 식으로 명령어들을 예측해서 실행하는 것을 '추측 실행 ( Speculative Execution )' 이라고 한다.
 
한가지 주의해야할 것은 분기 명령에는 종속성의 관계가 없다. 따라서, 미리 실행되어진다.
예를 들어,
C = A + B , D = C + 1 라는 연산을 수행할 때 A + B 로 C 를 먼저 정의한 후 D 를 연산하는 것이 종속성을 띄는 의미이며
if ( C == (A + B) ) { D = C + 1 } 같은 분기 과정에서 CPU 는 D = C + 1 을 미리 연산한다.
 
 
정리하자면, 성능 향상을 위해
순차적으로 처리하는 '동기 방식 ( Synchronous )''비동기 방식 ( Asynchronous )' 으로 처리하는 것으로 볼 수 있다.
 
또, 비순차적 기술과 분기 예측을 포함한 전체적인 과정을 'CPU 실행 파이프라인' 이라고 부른다.
 
 
 
 
 
성능적으로 최대한 효율을 내기 위해서 명령어를 미리 처리한다는 개념을 CPU 에 적용되어왔는데
이런 개념을 악용하는 사례들이 2018년에 발생한다.
바로 'CPU 게이트' 사건이다.
 
취약점의 원인에 따라 각각 Spectre, Meltdown 라는 명칭이 붙었다.
각각 사용된 취약점 개념은 앞서 살펴본 분기 예측 ( Branch Prediction ) 과 비순차적 실행 ( OoOE ) 이다.
 
Spectre 부터 살펴보자.
 
 
 

* Spectre

 

 

 

Spectre 같은 경우, 분기 예측 ( Branch Prediction ) 을 악용한 방법이다.
대표적인 분기로 if 문, switch 문 등이 존재하고 앞서 설명했듯이,
아직 분기를 들어설지 말지 판단하기 전에 미리 연산해놓음으로써 속도를 높이는데 이 때,
다른 메모리 영역에 접근하는 코드를 넣게 되면 그 작업이 미리 수행되며 캐시 메모리에 남겨진다.

 

 

https://blog.alyac.co.kr/1481

 

간단한 스펙터 예시 코드이다.
 
위처럼 선언된 배열의 크기를 넘어서는 인덱스를 참조해 데이터를 저장하려할 때,
조건은 맞지 않지만 미리 연산이 수행되어 잔여물이 캐시 메모리에 남겨지게 되며 해당 분기를 들어가는 명령은 취소되었지만
데이터를 저장하는 배열은 캐시 메모리에 남겨진 데이터를 참조하게 되는 아이러니한 상황이다.
이 때,
다른 쓰레드를 이용해 데이터를 저장하는 배열 각각의 데이터를 가져올 때 속도를 확인해서
상대적으로 빠르게 가져오는 데이터가 캐시 메모리에 남겨진 데이터라는 사실을 알 수 있다.
 
이러한 방식을 확장해서
실제 물리 메모리 페이지 크기를 넘어서서 다른 프로세스의 페이지를 볼 수 있다는 의미이다.
이런 의미로 '엿본다' 는 개념인 Spectre 라고 명칭되었으며 분기 에측을 사용한다는 의미에서
귀여운 Spectre 캐릭터 왼쪽 손에 나뭇가지 ( Branch ) 를 들고 있다.
 
 
사실 분기 예측 ( Branch Prediction ) 같은 개념 하나만으로 악용자가 자신에게 의미있는 데이터를 얻기란 굉장히 쉽지 않다고 한다.
또, CPU 입장에서도 속도를 향상시키기 위해서 완전히 배제할 수 없다고 한다.
분기 예측을 사용하는 것과 그렇지 않는 것의 성능 차이는 크다.
 
그리고 중요한 것은 분기 예측 개념 자체가 문제가 아니라
분기 예측이 실패했을 때, 명령어를 취소하는 과정에서 '캐시 메모리' 에 취소하기 전의 데이터 과정이 담겨진다는 것이다.
 
 
 
 
더욱 심각한 문제는 Meltdown 이었다.
 
 

* Meltdown

 

그림에서도 보듯이 방패마저 녹아버리게 할 정도로 강력한 부작용이었다.
위 Spectre 에 사용된 개념을 더욱 확장해 운영체제의 보안까지 위협하면서
일반적으로 사용자가 접근할 수 없는 운영체제가 관리하는 메모리에 접근할 수 있게 되는 것이다.

 

 

여러 자료를 참고해서 예상 시나리오를 그림으로 표현해보았다.
목적은 RAM 에서 운영체제가 쓰여지고 있는 메모리 안의 데이터이다.
 
우선, User Application ( Program ) 단에서 운영체제에게 많은 요청을 보낸다. 이를 'System call'이라고 한다.
운영체제가 관리하는 작업들을 요청할 수 있는 인터페이스 개념으로 보면 된다.
( 대표적으로 I/O 작업 등이 있다. )
이 때, 일부러 운영체제의 메모리에 접근하는 요청 등을 예외 상황을 발생하도록 유도한다.
이 때문에 많은 지연 발생한다.
또,
System call 자체도 응답 받기까지 생각보다 오랜 시간이 걸린다. 비동기 처리로 non-Blocking I/O 를 사용하는 이유이다.
CPU 는 이 작업을 기다리지 않는다. 비순차적 실행 개념을 이용해 당장 처리할 수 있는 것들을 처리하며
분기 예측으로 성공 시 더 빠른 수행을 위해 분기 내용을 미리 연산한다.
 
궁극적으로,
이렇게 미리 연산할 수 있는 시간을 확보하기 위해서 System call 로 운영체제에게 많은 지연 시간을 유발하고
이 때, 우리의 캐시 메모리로 연산된 데이터를 슬쩍 훔친다.
 
이 후, 앞서 본 것처럼 각 배열의 접근 속도 차이를 확인해서 빠른 속도로 데이터 접근이 가능하다면
그것이 캐시 메모리에 저장된 데이터라고 추측할 수 있다.
 
 
 
 
이 결함들은 거의 모든 인텔 CPU 매커니즘에 문제가 있는 것으로 확인되며
이외의 AMD 에서는 일부 제품에서만 확인되었다고 한다.
한동안 혼란이 계속 되었고 펌웨어 업데이트 이후의 속도 이슈 등이 추가적으로 생겨났다.
 
지금까지 살펴본 내용을 토대로 해결한 방법을 간단하게 유추해보자면,
운영체제에서 들어오는 System call 이 비정상적으로 많을 때 위험을 감지해 확인하거나
근본적인 문제인 명령어가 취소될 때 캐시 메모리에 저장된 과정도 같이 제거되는 과정을 추가했을 것이다.
 
 
현재에도 이를 이용한 변형들은 계속해서 나오고 있는 상황에서
우리가 해야할 것은 사전에 예방하려 노력하고 함께 문제를 해결해나가야할 것이다.
우리는 어떠한 혁신적인 변화와 가치로의 강점들은 최대한 활용하고 단점들은 보완해나가는 자세가 필요하다고 생각한다.

 

 

 

 

 


 

참고 자료