Chapter 3: Program의 machine level 표현 _Part 2

1231·2024년 9월 4일

컴퓨터 구조: CSAPP

목록 보기
4/4

Procedures (function)

x86-64 procedure가 register가 저장할 수 있는 것 이상으로 필요할때, stack 에 공간을 할당한다. 이러한 공간을 procedure의 stack frame 이라고 한다.

현재 실행되고 있는 ㄴprocedure의 frame이 항상 stack의 top에 위치한다.
Procedure P가 Q를 call할때, stack에 stack address를 push,
procedure는 가능하면 모든 정보를 register에 보관, stack을 확장하지 않으려고 한다.
따라서 stack frame가 필요없는 procedure가 존재하기도 한다.

Procedure의 Control Transfer

procedure P가 Q를 실행한 후, P에서 resume하기 위해서는 관련 정보가 stack에 저장되어 있어야 한다.
-> "call" inst 실행시 return address 가 stack push 된다(p의 stack frame으로 취급됨), 그 후 PC를 Q의 시작으로 바꾼다.

또한 "ret" inst 으로 해당 주소를 pop하고 PC를 해당주소로 설정한다.

얘네는 callq, retq로 표현되기도 하는데, 이는 크기를 나타내는것이 아니라 x86-64버전의 inst 임을 강조하는것이다.

Procedure의 작업이 끝난 후, 해당 stack frame 을 모두 deallocate 하여야 %rsp 가 Return address를 가르키게 되고, ret instruction 이 정상적인 작동을 할 수 있게 된다.

Procedure의 Data Transfer

x86-65 에서는 최대 6개의 argument를 register에 저장하여 전달 할 수 있다.
다음과 같은 순서로 register에 저장된다. 크기별로 register name이 달라진다.

만약 procedure가 7개 이상의 argument 를 가지면, 7 ~ n 의 argument들은 모두 stack에 저장된다.
P가 Q를 call할때 7개의 argument를 쓴다고 가정하자, 그러면 7 ~ n 의 argument를 위한 stack frame이 할당되어야한다.

argument 7은 stack의 top 에 위치하여야한다. 이러한 공간을 Argument build area 라고 한다.
stack 으로 argument를 전달할때는 8의 배수를 사용한다.

Local Storage on the stack

local variables 과 같은 것들은 "Local variables" 이라는 stack frame에 저장한다.

  • Local variables 만 추가로 할당할때
    1. stack pointer를 16만큼 decrementing
    2. Local variables arg1와 arg2를 offset 0과 8에 store
    3. 계산이 끝나면 stack pointer 를 incrementing 하여 dealloc

  • Local variables + 8개의 argument
    1. 각 variable의 크기를 고려 + 16 byte (2개의 argument)를 추가적으로 할당, 즉 예를 들어 32만큼 decrementing한다고 가정
    2. top 16byte에 argument, 나머지에 variables 저장
    3. 계산이 끝나면 stack pointer를 incrementing하여 dealloc

Local storage in Registers

어느 한 procedure(caller)가 다른 procedure(callee) 를 call했을 때,callee 는 caller가 후에 사용하기로 한 register의 값을 바꾸면 안된다.

convention에 의해, %rbx, %rbp, %r12~%r15 는 callee-saved register로써 callee 는 이값들을 그대로 caller에게 넘겨주어야한다.
-> 아예 건드리지 않거나 stack에 push했다가 다시 pop하는 방법을 사용한다. 이 register들의 값을 저장하는 stack frame segment를 "Saved Register" 라고 한다.

다른 Register들은 caller-saved Register로, caller가 알아서 얘네 값들을 보존하여야한다. 즉, callee 가 이 Register들의 값을 보장해주지 않는, caller가 알아서 save해야하는 Register.

Array Allocation & Access

T A[N] 은 두가지의 의미를 가진다.

  1. L * N byte의 메모리 할당. (L = size of a N)
  2. A는 array의 시작점 pointer, x_A로 표기하겠음.
    -> "i" index의 element는 x_A + L * i 주소에서 찾을 수 있다.

x86-64에서는 다음과 같이 접근 가능.
E[i], where the address of E is stored in register %rdx and i is stored in register %rcx.
movl (%rdx,%rcx,4),%eax

Pointer Arithmetic

C는 pointer에 산수를 수행할 수 있다는 특징을 가지고 있다.
p = pointer of data type T
x_p = value of p(주솟값임, dereferencing이 아니다.)

-> p + i = x_p + L i (이렇게 T의 size에 따라 자동으로 scale된다.)
A[i] =
(A+i)

Multidimension Array

T D[R][C] 는 다음과 같이 접근한다: &D[i][j] = x_D+ L(C * i + j)
"i" 는 row
"j" 는 column
"C" 는 column의 수

A[3][1]은 x_A + L(3 * 3 + 1)

Assembly에서의 접근법은 다음을 참고.

Optimiazation of Fixed-size array

array 접근을 모두 pointer dereference 로 대체하여 최적화를 수행.

최대한으로 array 접근 수식(x+L(C*i+j))를 피함.

Data Structures

Struct

여러 다른 type이 하나의 object로 합쳐진 struct 의 경우 array와 비슷한 방법으로 구현된다.
-> 각 구성요소가 메모리에 연속(contigious)해서 존재 & pointer 는 첫 byte

위의 struct는 다음과 같이 저장된다.

rec->i 에서 rec->j로 값을 옮기려고 한다면 다음과 같다.

&(rec->a[i]) 를 구하려고 한다면, 단순히 주소에 수를 더해주면 된다.

이렇듯 컴파일시에는 struct의 필드가 결정된다. 최종 machine code는 필드 선택, 선언, 이름 모두 알 수 없다.

Unions

signle object가 여러개의 타입으로 referenced 될 수 있도록 함.
각 필드가 다른 메모리 block을 가지는 것이 아니라 전부 하나의 block을 reference 함.

struct와 union의 차이:

struct는 3개의 block을 가지고 있지만,
union은 1개의 block만을 가지고 있다.

이 유형은 필드가 상호배타적(mutual exclusive)일때 주로 사용된다.
즉, 각 필드중 하나만 사용되는 각각의 상항이 있을때, struct를 만드는데 필요한 공간을 줄이기 위해서 사용된다.

ex) binary tree data structure 를 구현하고자 한다고 가정하자.
leaf node 는 2개의 data 를 가지고,
internal node 는 data를 가지지 않고 children node 를 가르키는 left/right pointer만 가진다.

이를 다음과 같이 struct로 구현하면,

32byte를 사용하고,

다음과 같이 union으로 구현하면

16byte만 사용한다. 이 노드 구조체는 leaf거나 internal. 둘중 하나이므로, field 또한 둘중 하나만 가지고 있으면 된다. 이 특징을 사용하여 불필요한 block의 할당을 방지한다.

근데 이 경우, 해당 노드가 leaf 인지 internal인지 결정할 수 없으므로 enumerated type 을 정의하여 union 에 태그 필드를 추가한다.

그러면 type 4 byte + padding 4 byte + left/right or data 16 byte = 24 byte를 할당한다.

Data alignment

각 object가 K의 배수 (일반적으로 k = 2,4,8) 이라면 read와 write가 간단해진다.

->예를 들어 processor가 8byte 단위로 r/w를 수행한다고 가정하자, 그러면 double 이라는 type이 저장되는 주소가 항상 8의 배수로 일정하다면, r/w시에 최소의 operation을 수행할 수 있다. 하지만 그렇지 않고 8byte block에 걸쳐서 쪼개진다면, 불필요한 추가적인 operation의 수행이 강제된다.

따라서 structure 를 위한 주소를 할당할때는 이러한 "alignment requirement" 를 만족시키기 위해서 gap을 추가한다.

아래와 같이 추가적인 3byte의 padding.

Pointers

Out-of-bound Memory References and Buffer overflow

C는 array reference 에 대한 bound check를 수행하지 않는다.

-> 따라서 out-of-bound write은 stack을 overwrite할 수 있고 이는 saved register 나 return address 영역의 corruption 으로 이어질 수 있다.
이를 buffer overflow라고 한다.

여기서 gets 에 넣는 8번째 문자부터 array를 위해 할당된 stack 부분의 바깥범위를 침범하게 된다.

9~23은 Unused stack space로 침범해도 어느정도 괜찮지만, 그 위로는 위험하다.

이러한 buffer overflow는 해킹에 악용된다.
공격을 막거나 공격당해씅ㄹ 시 침입자가 권하을 가지는것을 제한하는 방버들이 구현되고 있고, 다음은 gcc에서 제공하는 메커니즘들임.

  1. Stack Randomization
    공격하기 위해서는 코드와 그코드의 pointer를 inject하여야 한다.
    이 pointer를 만들기 위해서는 string이 저장되는 stack 주소를 알아야함.
    -> stack이 매핑되는곳을 randomization 하여 이를 방지한다.

  2. Stack Corruption Detection
    local buffer와 stack의 나머지 부분 사이에 canary value를 추가하여 buffer overflow를 감지한다.
    canary value 혹은 guard value라고 한다.
    -> register 값이나 return 하기전에 이 canary 값이 손상/변경되었는지 확인한다. (canary value는 %fs라는 special registe에 저장한다.)
    gcc는 function에 이 기능이 필요한지 자동으로 판단 후 추가한다.

  3. Limiting Executable code regions
    컴파일러에 의해 생성된 코드가 있는 부분의 메모리만 실행 가능하면 된다.
    x86는 readble 일때 동시에 executable하게 설계 되었다.(과거)
    -> readable이지만, unexecutable 하게 하는 방법이 있지만 비효쥴적이다. 최근에는 이 두개의 권한을 분리하여 설계한다.

Variable-size Stack Frame

malloc을 통한 dynamic allocation을 수행할때나 variable size의 array을 선언할때 stack frame은 variable 한 양의 local storage를 필요로 한다.

예를 들면)

p는 n에 따라서 그 크기가 달라진다.
이러한 variable-size Stack을 지원하기 위해서 %rbp를 "frame pointer"로 사용한다. (또는 "base register") 라고 하기도함.

variable-size의 object를 위한 스택을 할당하기 전에, %rbp에 현재 stack pointer를 저장해놓는다.
%rbp callee-saved Register이기 때문에 사용전에 값을 세이브해놔야한다.
그후 movq %rsp, %rbp 구문을 통해 현재 위치를 pointing 한다.
-> 모든 local 데이터를 frame pointer의 offset로 접근할 수 있어야한다.

모든 값이 고정이라면, sp로 충분한 접근이 가능하지만, variable size는 FP로 가능하다.

leaq 15(,%rdi,8), %rax = 15를 더해서 최적의 16배수로 반내림될 수 있도록함.32 + 15 = 47 반내림시 32, 31 + 15 = 46 반내림시 32
andq $-16, %rsp = round down to multiple of 16

Floating-Pointer Code

데이터 병렬 처리를 지원하는 single instruction multiple data, SIMD
-> 같은 작업을 여러 데이터에 수행할때 좋은 성능을 낼 수 있게 한다.

모델명 MMX -> SSE -> AVX 순으로 변경되었음.

"MM" Register for MMX, 64 bit
"XMM" Register for SSE, 128 bit
"YMM" Register for AVX, 256 bit

(~15까지 있음.)

이러한 media instruction(주로 media 처리에 사용되는 instruction이라 이렇게 부름)은 scalar floating-point data 의 처리를 지원한다.

low-order 32/64 bit of XMM/YMM Register를 사용한다.

AVX Floating-point architecture는 16개의 YMM register에 데이터를 저장한다.
%ymm0 ~ %ymm15 각 256(32bytes) 길이 ,
scalar 값을 저장할때는 low-order 32(float)/64(double) bit 사용

Floating-pointer Conversion/Movement

"VMOV" insturction class로 데이터 이동이 이루어진다.

memory reference를 통해 이루어지는 instruction은 모두 scalar instruction 이다.

floating-pointer number <-> Integer 간의 convert는
"VCVT" instruction class를 사용한다.

floating-point 값에서 integer로 변환될때, truncation을 수행한다.
rounding values toward zero

integer -> floating number 변환시에는 operand 3개가 사용되는데, 2번째 operand는 결과값의 upper bit 에만 영향을 주므로, 단순히 3번째와 동일하게 작성하면 된다.

Single <-> Double 변환시에도 다음과 같은 instruction이 존재하는데,
vcvtsi2sdq %rax, %xmm1, %xmm1
vcvtss2sd %xmm0, %xmm0, %xmm0
GCC에서는 instruction을 사용하기보다 다르게 코드를 생성한다.

Single to Double by GCC

vunpcklps 는 source들의 low-order값을 섞는다고 보면 된다.
[s3, s2, s1, s0] , [d3, d2, d1, d0] -> [s1, d1, s0, d0].
vcvtps2pd는 low-roder 값 2개를 double로 변환한다.
[Ds0, Dd0]
s0와 d0이 64bit double로 변환된다.

Double to Single by GCC

vmovddup 는 [x1,x0] -> [x0, x0]
32 32 64 64 bit
vcvtpd2psx는 얘네를 single로 변환하는데, 남은 자리를 0으로 채운다

[0.0, 0.0, Sx0, Sx0]
32 32 32 32 bit

Procedures 에서의 Floating Point

함수의 return 값은 언제나 %xmm0 에 저장된다.

  1. floating-point argument는 총 8개 가능하다. %xmm0 ~ %xmm7, 나머지는 stack에 저장된다.
  2. %xmm0 으로 return
  3. 모든 xmm register는 caller-saved 로 overwritten 가능하다.

float과 다른 type이 혼용될때는 다음과 같다

double f1(int x, double y, long z);
각 argument는 %edi %xmm0 %rsi 순으로 저장된다.

Floating-point number arithmetic

vaddss, vsubss 와 같은 floating-point 계산 전용 instruction을 사용한다.
source는 memory/register, destination은 무조건 register 이다.

int와 float계산시 int를 float으로 변환 후 계산한다.

immediate(상수)값을 operand로 가질 수 없으므로,
-> 메모리에 저장 -> 메모리에서 읽어옴 이런식으로 상수값을 활용한다.


LC2는 1.8
-> 3435973837 (0xcccccccd) and 1073532108 (0x3ffccccc.)
Exponent value = 0x3ff(1023) -1023(bias) = 0
Fraction value = 0xccccccccccccd(0.8)
implied leading 1 을 더하면 1.8 이 된다.

LC3는 32.0 이다.
-> 같은 방법으로 계산해보면 됨.

0개의 댓글