※ 선형 자료구조

앞서 우리가 배운 것들은 모두 선형 자료구조에 속하는데 대표적으로 다음과 같다.

- stack, queue, deque, list . . . 

이와 달리 선형이 아닌 "계층적 구조 (hierarchical)"를 표현하기 위해 사용되는 자료구조인 트리(tree)에 대해 알아보고자 한다.

 

컴퓨터 저장장치의 디렉토리 구조도 계층적인 자료구조로 많은 상황에서 사용되기에 자료구조를 배우는 이유중 하나라 할 수 있다.

자료구조의 양대산맥이라 생각될 정도로 그래프만큼 많이 사용되며 자주 사용되는 자료구조이기에 잘 익혀두자!

 

 

※ 트리 용어 

트리의 구성요소로는 루트(root), 서브트리(subtree), 간선(edge), 노드(node) 등이 있다.

- 차수(degree): 어떤 node가 갖는 자식노드의 개수

- 레벨(level): 트리의 층 수로 root를 level 0이나 1로 매긴다.(책마다 조금씩 다름)

- 높이(height): 트리의 각 층에 번호를 매기는 것으로 트리의 높이는 최대 레벨을 통해 결정할 수 있다.

 

 

※ 이진트리 (Binary Tree)

모든 노드의 차수가 2 이하이며 왼쪽 서브트리와 오른쪽 서브트리가 반드시 구별되어야 한다.

n개의 노드는 n-1개의 간선(edge)를 가지며 높이가 h인 이진트리의 최대 노드 개수는 2^h - 1이다.
n개의 노드를 갖는 이진트리의 높이는 다음과 같다. ⎡log(n+1)⎤ ≤ h ≤ n
포화이진트리(full binary tree): 모든 레벨의 노드가 꽉 차있는 트리, 높이가 h라면 총 2^h-1개의 노드를 갖는다.
완전이진트리(complete binary tree): 레벨 1~h-1까지는 모두 노드가 채워져 있고 마지막 레벨 h는 왼쪽~>오른쪽으로 순서대로 채워진 트리이다. (즉, 마지막 레벨의 노드가 꽉 차있지 않아도 됨)

대표적인 완전이진트리 예로는 힙(heap)이 있다.

 

 

 

※ 이진트리 순회 알고리즘

✯ 전위 순회 (preorder)

preorder(x)

if x != null
	then print data(x);	// 루트(x)노드 처리 
    	 preorder(LEFT(x));  // 왼쪽 subtree 방문
         preorder(RIGHT(x)); // 오른쪽 subtree 방문

 

✯ 중위 순회 (inorder)

inorder(x)

if x != null
	then inorder(LEFT(x));	// 왼쪽 subtree 방문
         print data(x);     // 루트(x)노드 처리
    	 inorder(RIGHT(x)); // 오른쪽 subtree 방문

 

✯ 후위 순회 (postorder)

postorder(x)

if x != null
	then postorder(LEFT(x)); // 왼쪽 subtree 방문
         postorder(RIGHT(x)); // 오른쪽 subtree 방문
         print data(x);     // 루트(x)노드 처리

 

 

 

 

 

※ 이진트리 노드 개수 및 높이 구하는 알고리즘

✯ node 개수 구하기

getCount(x)

if x == null
	then return 0;
    else return 1 + getCount(LEFT(x)) + getCount(RIGHT(x));

 

✯ leaf node 개수 구하기

getLeafCount(x)

if x == null 
	then return 0;
if LEFT(x) == null and RIGHT(x) == null
	then return 1;
    else return getLeafCount(LEFT(x)) + getLeafCount(RIGHT(x));

 

✯ height 구하기

getHeight(x)

if x == null
	then return 0;
    else return 1 + max(getHeight(LEFT(x)), getHeight(RIGHT(x)));

 

 

 

 

 

 

※ 이진트리 구현

✯ BinaryNode.h

#include <iostream>

class BinaryNode{
protected:
    int data;
    BinaryNode* left;
    BinaryNode* right;
public:
    BinaryNode(int val = 0, BinaryNode *l = nullptr, BinaryNode *r = nullptr) : data(val), left(l), right(r) { }
    void setData(int val) { data = val; }
    void setLeft(BinaryNode* l) { left = l; }
    void setRight(BinaryNode* r) { right = r; }
    int getData() const { return data; }
    BinaryNode* getLeft() const { return left; }
    BinaryNode* getRight() const { return right; }
    bool isLeaf() const { return left == nullptr && right == nullptr; }
};

 

✯ BinaryTree.h

#include "BinaryNode.h"
using namespace std;

class BinaryTree {
private:
    BinaryNode *root;
public:
    BinaryTree() : root(nullptr) { };
    void setRoot(BinaryNode * node) { root = node; }
    BinaryNode * getRoot() { return root; }
    bool isEmpty() { return root == nullptr; }

    // 이진트리의 순회연산 (전위, 중위, 후위, 레벨 순회)
    void inorder() { cout << endl << "inorder: "; inorder(root); }
    void inorder(BinaryNode *node) {
        if (node != nullptr) {
            inorder(node->getLeft());
            cout << node->getData() << " ";
            inorder(node->getRight());
        }
    }
    void preorder() { cout << endl << "preorder: "; preorder(root); }
    void preorder(BinaryNode *node) {
        if (node!= nullptr) {
            cout << node->getData() << " ";
            preorder(node->getLeft());
            preorder(node->getRight());
        }
    }
    void postorder() { cout << endl << "postorder: "; postorder(root); }
    void postorder(BinaryNode *node) {
        if (node!= nullptr) {
            postorder(node->getLeft());
            postorder(node->getRight());
            cout << node->getData() << " ";
        }
    }

    // 이진트리의 추가 연산 (이진트리 노드의 개수 구하기 ,단말노드개수 구하기, 높이 구하기)
    int getCount() { return isEmpty() ? 0 : getCount(root); }
    int getCount(BinaryNode *node) {
        if (node == nullptr) return 0;
        return 1 + getCount(node->getLeft()) + getCount(node->getRight());
    }
    int getLeafCount() { return isEmpty() ? 0 : getLeafCount(root); }
    int getLeafCount(BinaryNode *node) {
        if (node == nullptr) return 0;
        if (node->getLeft()) return 1;
        else
            return getLeafCount(node->getLeft()) + getLeafCount(node->getRight());
    }
    int getHeight() { return isEmpty() ? 0 : getHeight(root); }
    int getHeight(BinaryNode *node) {
        if (node == nullptr) return 0;
        int hLeft = getHeight(node->getLeft());
        int hRight = getHeight(node->getRight());
        return (hLeft > hRight) ? hLeft + 1 : hRight + 1;
    }
};

 

✯ main.cpp

#include "BinaryTree.h"
#include <iostream>
using namespace std;

int main() {
    BinaryTree tree;
    BinaryNode *d = new BinaryNode('D', nullptr, nullptr);
    BinaryNode *e = new BinaryNode('E', nullptr, nullptr);
    BinaryNode *b = new BinaryNode('B', d, e);
    BinaryNode *f = new BinaryNode('F', nullptr, nullptr);
    BinaryNode *c = new BinaryNode('C', f, nullptr);
    BinaryNode *a = new BinaryNode('A', b, c);
    tree.setRoot(a);

    tree.inorder();
    tree.preorder();
    tree.postorder();
    cout << endl;

    cout << "노드의 개수 = " << tree.getCount() << endl;
    cout << "leaf의 개수 = " << tree.getLeafCount() << endl;
    cout << "tree의 높이 =  " << tree.getHeight() << endl;


    /********************************************************************/
    //inorder: 68 66 69 65 70 67
    //preorder: 65 66 68 69 67 70
    //postorder: 68 69 66 70 67 65
    //노드의 개수 = 6
    //leaf의 개수 = 1
    //tree의 높이 =  3
}

 

 

이번주의 image 주제

 image = A partial view of Space ! 

 즉, 공간의 "일부 시점"에 불과하다는 관점!

 

 

 

 

※ scaling 

Scale: 사진의 크기를 확대하거나 줄이는 것.

밝기값을 찾기 위해 원본 image를 vectorize하여 밝기값을 구하고 rasterize를 다시 시켜 scale을 진행한다.
pixel과 pixel사이를 채워주는 것을  의미.
• 예를 들어 a만큼 scale을 한다면
 (x, y) 위치의 값으로 들어간다. (new) ← (ax , ay) 의 값을(original)이를 rasterize시키면 된다.

 

 

 

 [실습] - vectorization을 위해 Bilinear interpolation 이용 

§ 확대하기

 

§ 축소하기

 

 

※ Rotate 실습

 

 

 

 

 

 

[과제]

과제 1) 크기 손상 없이 그대로 rotation 하기 - for i in range(H-1) 부분에서 1픽셀 잘림

과제 2) for문 대신 선형대수 식으로 변형해서 구현해보기

과제 3) bilinear 대신 bicubic 또는 triangulation 방식으로 해보기

 

※ tokenization이란?

토큰화(tokenization)문장을 토큰으로 나누는 과정으로 수행대상에 따라 다음 세 방법이 있다.
- 문자
- 단어
- 서브워드
§ 문자 단위 토큰화
-  한글 위주로 표현된 데이터로 언어 모델을 만든다 하자.
한글 음절 수는 1만1172개이므로 알파벳, 숫자 등을 고려 해도 어휘 집합 크기는 최대 1만5000개를 넘기 어렵다.
게다가 해당 언어의 모든 문자를 어휘 집합에 포함하므로 미등록 토큰(unknown token) 문제로부터 자유롭다.
미등록 토큰이란 어휘 집합에 없는 토큰이다. (주로 신조어 등에서 발생)

하지만 문자 단위로 토큰화를 수행할 경우 단점도 있는데, 각 문자 토큰은 의미 있는 단위가 되기 어려운데,  예를 들어 어제와 어미간의  어의 구분이 모호해지는 것처럼 단점이 존재한다.
어제 카페 갔었어          > 어 / 제 / 카 / 페 / 갔 / 었 / 어
어제 카페 갔었는데요   > 어 / 제 / 카 / 페 / 갔 / 었 / 는 / 데 / 요

뿐만 아니라 문자 단위 토큰화는 앞의 단어 단위와 비교할 때 분석 결과인 토큰 시퀀스의 길이가 상대적으로 길어졌음을 확인할 수 있다. 언어 모델에 입력할 토큰 시퀀스가 길면 모델이 해당 문장을 학습하기가 어려워지고 결과적으로 성능이 떨어지게 된다.
§ 단어(어절) 단위 토큰화
- 가장 쉽게 생각해보면 공백으로 분리할 수 있다. 
어제 카페 갔었어          > 어제 / 카페 / 갔었어
어제 카페 갔었는데요   > 어제 / 카페 / 갔었는데요

공백으로 분리하면 별도의 토크나이저를 쓰지 않아도 된다는 장점이 있다.
다만, 어휘 집합의 크기가 매우 커질 수 있는데,  갔었어, 갔었는데요 처럼 표현이 살짝만 바뀌어도 모든 경우의 수가 어휘 집합에 포함돼야 하기 때문이다.

만약 학습된 토크나이저를 사용하면 어휘 집합의 크기가 커지는 것을 조금 완화할 수는 있다.
예를 들어 같은 문장을 은전한닢으로 토큰화하면 다음과 같다.
예시가 적어서 효과가 도드라져 보이지는 않지만, 의미 있는 단위로, (예를 들면 갔었) 토큰화해 어휘 집합이 급격하게 커지는 것을 다소 막을 수 있다.
어제 카페 갔었어 > 어제 / 카페 / 갔었 / 어
어제 카페 갔었는데요 > 어제 / 카페 / 갔었 / 는데요

그렇지만 위 같은 토크나이저를 사용하더라도 어휘 집합 크기가 지나치게 커지는 것은 막기 어려운데,  보통 언어 하나로 모델을 구축할 때 어휘 집합 크기는 10만 개를 훌쩍 넘는 경우가 다반사이기에 어휘 집합 크기가 커지면 그만큼 모델 학습이 어려워질 수 있다.
§ 서브워드 단위 토큰화
- 단위 토큰화는 단어와 문자 단위 토큰화의 중간에 있는 형태로. 둘의 장점만을 취한 형태이다.
어휘 집합 크기가 지나치게 커지지 않으면서도 미등록 토큰 문제를 피하고,
분석된 토큰 시퀀스가 너무 길어지지 않게 한다.
- 대표적인 서브워드 단위 토큰화 기법이라면 바이트 페어 인코딩을 들 수 있는데 아래에서 소개하겠다.

 

 

※ Byte Pair Encoding  Algorithm

- BPE는 정보를 압축하는 알고리즘으로 원래 제안된 것으로 데이터에서 최빈문자열을 병합해 압축하는 기법이다.

최근에는 자연어 처리모델에서 쓰이는 토큰화 기법이다.

GPT 모델은 BPE기법으로 토큰화를 수행

BERT모델은 BPE와 유사한 워드피스(wordpiece)를 토크나이저로 사용

 

예를 들어 다음과 같은 데이터가 있다고 가정하자.

  • aaabdaaabac

BPE는 데이터에 등장한 글자(a, b, c, d)를 초기 사전으로 구성하며, 연속된 두 글자를 한 글자로 병합한다.

이 문자열에선 aa가 가장 많이 나타났으므로 이를 Z로 병합(치환)하면 다음과 같이 압축할 수 있다.

  • ZabdZabac

이 문자열은 한번 더 압축 가능한데,  ab가 가장 많이 나타났으므로 이를 Y로 병합(치환)한다.

  • ZYdZYac

물론 ab 대신 Za를 병합할 수도 있지만 둘의 빈도수가 2로 같으므로 알파벳 순으로 앞선 ab를 먼저 병합한다.

ZY 역시 X로 병합할 수 있습니다. 이미 병합된 문자열 역시 한 번 더 병합할 수 있다는 얘기로 다음과 같다.

  • XdXac

 

※ BPE 어휘집합 구축하기.

1. pre-tokenize 진행 (말뭉치를 준비 후 모든 문장을 공백으로 나눠줌. 물론 다른 기준으로 나눌 수도 있음)

2. pre-tokenize 진행 후 그 빈도를 모두 세어 초기 어휘 집합을 구한다.

3. 위의 어휘집합을 바이그램 쌍(토큰을 2개씩 묶어 나열)으로 만들고 바이그램 쌍을 합친다.

4. 가장 많이 등장한 바이그램쌍을 합쳐 집합에 추가 (위를 예로 들면 u, g를 합친 ug를 바이그램에 추가)

>> b / g / h / n / p / s / u / ug

이후 계속 사용자가 정한 크기 전까지 반복해서 진행.

 

5. 입력이 들어오면 입력을 문자단위로 분리 후 병합의 우선순위(by 빈도)에 따라 merge 진행.

만약 input으로 mug가 들어온다면?

따라서 출력은 다음과 같이  <unk>, ug로 나오며 <unk>는 unkown token(미등록 토큰)을 의미.

 

 

 

※ WordPiece.

BPE와 비슷하지만 빈도를 기준으로 merge를 진행하는 BPE와는 달리

Wordpiece는 likelihood를 기준으로 likelihood가 높은 쌍을 merge한다.

 

병합후보 A, B에 대해 워드피스의 병합기준 식은 다음과 같다.

이 식의 값이 높은 값을 기준으로 병합을 진행하며 

n은 전체 글자수를 가리키고 a, b, ab는 각각의 문자열의 빈도수를 뜻한다.

즉, a,b는 각각의 문자가 등장할 확률이고 ab는 ab가 연이어 등장할 확률이다.

 

 

 

 

 

※ 자연어와 자연어 처리

자연어(natural language)란 우리가 일상 생활에서 사용하는 언어로 자연어 처리(natural language processing)란 이러한 자연어의 의미를 분석하여 컴퓨터가 처리하는 것이다.
좀 더 구체화 시키면 입력(자연어)을 받아서 해당 입력이 특정 범주일 확률을 반환하는 확률함수이다.
이런 확률을 기반으로  후처리(post processing)을 해서 자연어형태로 바꿔줄 수 도 있다.

자연어 처리는 음성 인식, 내용 요약, 번역, 사용자의 감성 분석, 영화 평론의 긍정 및 부정, 텍스트 분류 작업(스팸 메일 분류, 뉴스 기사 카테고리 분류), 질의 응답 시스템, 챗봇과 같은 곳에서 사용되는 분야 등 다양하다.

이런 자연어 처리에 다양한 모델이 사용되고 요즘 가장 인기있는 모델은 단연, 딥러닝이다.
딥러닝 가운데서도 딥러닝 기반 자연어 처리 모델에는 BERT, GPT 등이 있다.

 

 

※ Task란?

- 준비한 모델, 최적화 방법 및 학습과정 등이 정의되어 있는 것.
- 우리는 특정 조건에서 모델의 output과 정답의 차이를 작게하는게 중요, optimizer, learning rate scheduler를 정의.

모델 학습은 batch단위로 이뤄지는데, batch를 모델에 입력한 후 모델 출력을 정답과 비교해 차이를 계산한다.
그 후 차이를 최소화하는 방향으로 모델을 update하는데, 이런 일련의 과정을 step이라 하며 task의 학습과정은 1step을 기준으로 정의한다.

 

 

 

 

※ Transfer Learning

특정 data를 학습한 모델을 다른 data train을 할 때 재사용하는 방법

- 장점: 학습속도가 기존보다 빠르고 새로운 data를 더 잘 수행할 수 있어 BERT, GPT 등도 transfer learning이 적용된다.

 

§ Upstream Task
- Transfer Learning이 주목된 것은 upstream task와 pretrain덕분으로 자연어의 다양한 context를 모델에 내재화하고 다양한 down stream task에 적용해 성능을 끌어올렸기 때문이다.
이는 기존의 지도학습과 달리 다량의 학습데이터를 웹문서, 뉴스 등의 쉽게 구할 수 있는 데이터와 이를 upstream task를 통해 수행하여 성능이 월등히 좋아졌다.
즉, 데이터 내에서 정답을 만들고 이를 통해 모델을 학습하는 자기지도학습(self-supervised learning)을 진행.

▶ Masked Language Model
- 아래와 같이 빈칸에 들어가야할 단어에 대해 주변 문맥을 보고 해당 빈칸의 단어에 해당하는 확률은 높이고
나머지 단어들의 확률을 낮추는 방향으로 모델 전체를 update한다.

 

 

§ Downstream Task
- 위에서 진행한 upstream으로 pretrain을 한 이유downstream task을 잘하기 위해서이다.
  이런 downstream task는 자연어처리의 구체적인 과제들이다.
  예를 들자면 분류(classification)처럼 입력이 어떤 범주에 해당하는지 확률형태로 반환한다.
- 문장생성을 제외대부분은 pretrain을 마친 Masked Language Model (BERT, etc.)을 사용한다.

ex. 문서분류, 자연어 추론, 개체명 인식, 질의응답, 문장생성 등에 대해 처리한다.


▶ Fine-tuning
- downstream task의 학습방식 중 하나로 pretrain을 마친 모델을 downstream task에 맞게 update하는 기법이다.

https://ratsgo.github.io/nlpbook/docs/introduction/transfer/#%EB%8B%A4%EC%9A%B4%EC%8A%A4%ED%8A%B8%EB%A6%BC-%ED%83%9C%EC%8A%A4%ED%81%AC

 

 

 

 

 

 

 

※  Linux 기본명령어

※  Linux  권한 관련 기본명령어 

 

 

 

 

 

 

 

※  Linux 기본명령어

 

※ Memory 참조 버그 예제

typedef struct {
    int a[2];
    double d;
} struct_t;

double func(int i) {
    volatile struct_t s;
    s.d = 3.14;
    s.a[i] = 107374824;
    return s.d;
}

 

 

cf. volatile 변수 소개

C/C++ 프로그래밍 언어에서 최적화 등 컴파일러의 재량을 제한하는 역할을 한다.
개발자가 설정한 개념을 구현하기 위해 코딩된 프로그램을 온전히 컴파일되도록 한다.
주로 최적화와 관련하여 volatile가 선언된 변수는 최적화에서 제외된다.
OS와 연관되어 장치제어를 위한 주소체계에서 지정한 주소를 직접 접근하는 방식을 지정할 수도 있다.
Linux Kernel 등의 OS에서 메모리 주소는 논리주소와 물리주소 간의 변환이 이루어진다.
경우에 따라 이런 변환을 제거하는 역할을 한다. 또한 원거리 메모리 점프 기계어 코드 등의 제한을 푼다.
C언어의 경우, 주로 메모리 맵 입출력(MMIO)을 제어할 때,
volatile을 선언한 변수를 사용하여 컴파일러의 최적화를 못하게 하는 역할을 한다.
static int foo;

void bar(void) {
    foo = 0;
    while (foo != 255);
}
foo의 값의 초기값이 0 이후, while 루프 안에서 foo의 값이 변하지 않기 때문에 while의 조건은 항상 true가 나온다. 따라서 컴파일러는 다음과 같이 최적화한다.
void bar_optimized(void){
    foo = 0;
    while (true);
}

이렇게 되면 while의 무한 루프에 빠지게 된다. 이런 최적화를 방지하기 위해 다음과 같이 volatile을 사용한다.

static volatile int foo;

void bar (void) {
    foo = 0;
    while (foo != 255);
}
이렇게 되면 개발자가 의도한 대로, 그리고 눈에 보이는 대로 기계어 코드가 생성된다.
이 프로그램만으로는 무한루프라고 생각할 수 있지만, 만약 foo가 하드웨어 장치의 레지스터라면 하드웨어에 의해 값이 변할 수 있다.
따라서 하드웨어 값을 폴링(polling)할 때 사용할 수 있다.

 

※ Memory 참조 버그 (= Buffer Overflow)의 심각성

배열에 할당된 크기 이상의 메모리를 접근할 때 주로 발생한다.

가장 빈번하게 발생하는 보안 취약성의 원인이 된다.

§ 가장 일반적인 형태로는 다음과 같다.
- string 입력의 길이를 check하지 않은 경우 
- stack에 생성되는 제한된 길이의 문자배열

 

§ UNIX의 gets 함수 (키보드를 관리해주는 라이브러리)

/* Get string from stdin */

char *gets (char *dest) {
	int c = getc();
    char *p = dest;
    while (c != EOF && c != '\n') {
    	*p++ = c;
        c = getc();
	}
    *p = '\0';
    return dest;
}
cf. 유사한 문제
- strcpy: 임의의 길이의 string을 복사
- scanf, fscanf, sscanf 함수를 %s와 함께 사용하는 경우

 

 

§ 위험한 buffer 코드

/* Echo Line */
void echo() {
	char buf[4];
    gets(buf);
    puts(buf);
}


int call_echo () {
	printf("Type a string: ");
    echo();
    return 0;
}

 

§ stack 상태

 

 

 

 

§ Buffer Overflow를 피하는 방법

※ string의 길이를 제한하는 라이브러리를 사용하면 된다! ( buffer 주소를 주면서 buffer크기를 check)

gets대신 fgets를 사용  
strcpy대신 strncpy를 사용
scanf를 %s와 함께 사용하지 않는다.  ( fgets를 사용한다. )
/* Echo Line */

void echo() {
	char buf[4];
    fgets(buf, 4, stdin);
    puts(buf);
}

※ procedure 목차

1. 스택의 구조

2. 호출의 관습

  - 제어의 전달

  - 데이터의 전달

  - 지역 데이터의 관리

3. 재귀실행

 

§ Procedure 소개

프로시저(procedure)는  특정 작업을 수행하는 서브 프로그램으로 자주 사용하기 위해 생성하는 메서드 같은 것이다.

이런 프로시저의 실행에는 3가지 유형이 있다.

 

§ 프로시저 실행의 유형 (모든 동작은 기계어로 구현된다.)

1. 제어의 전달

  ▷ 프로시저 코드의 시작부분으로 리턴지점으로 돌아간다.

2. 데이터의 전달

  ▷ 프로시저 인자

  ▷ return 값

3. 메모리 관리

  ▷ 프로시저 실행 중 할당하며 return시 반환한다.

 

 

 

 

 

 

 

※ procedure _ 2. 호출의 관습

§ x86-64 스택

- 메모리 영역으로 register %rsp는 가장 작은 스택주소를 저장.

 

 

 

 

§ procedure 제어흐름

▶ procedure 호출

stack frame을 이용해 procedure호출과 return을 지원하며 아래와 같은 호출 방법을 사용한다.

call Label

- Step 1: return 주소를 stack에 push

- Step 2: Label로 jump

cf) return 주소란?
call 바로 다음 줄의 명령어 주소로 아래 예시를 들자면, 400549가 바로 return 주소이다.

400544:  callq  400550  <mult2>
400549:  mov  %rax,  (%rbx)

 

▶ procedure 반환

stack frame을 이용해 procedure반환을 진행하는데, stack에서 return주소의 pop을 진행하며 아래와 같은 방법 사용하며, 이 return 주소로 jump한다는 의미이다.

ret

 

 

 

 

 

§ procedure 데이터의 흐름

이때, Stack Frame은 필요할 때에만 할당한다.

 

 

 

§ procedure 지역 데이터의 관리

▶ Stack Frame 기반 언어 = 재귀호출을 지원하는 언어로 C, Java와 같은 언어들을 예시로 들 수 있다.

- "Reentrant", 즉 재진입해야하는 코드로 각 실행개체의 상태를 저장할 장소가 필요하기 때문이다.

 

Ex. Recursion의 Stack Frame

 

 

▶ x86-64 / Linux 의 Stack Frame 

 

Ex-1).  incr 함수

 

Ex-2).  incr 함수

- incr #1

subq	$16,  %rsp         #rsp-16 = stack이 커진다 = stack frame할당으로 공간확보
movq	$15213,  8(%rsp)   #15213을 v1에 copy

- incr #2

movl	$3000,  %esi         # rsi에 3000을 넣음
leaq	8(%rsp), %rdi        # rdi에 주소 할당

- incr #3  (15213 + 3000을 더해 18213값이 들어감)

 

- incr #4

addq	8(%rsp),  %rax         # 현재 incr의 return값 rax는 v2에 저장되어 있어서 v1+v2를 다음과 같이 진행
addq	$16, %rsp              # pop을 진행

 

 

 

 

▶ register 보관관습 => stack frame에 필요한 register를 push해 저장함

- 똑같은 register를 다른 함수에서 사용가능해서 return 후 다시 register를 복원한다.

- 아래 예시를 보면 두 개 모두 %rdx라는 중복된 register를 사용한다.

 

때문에 compiler에 따라 다음 2가지 방식으로 구현할 수 있다.

 

 

 

 

 

 

§ 재귀 실행

Ex)

/*-----------Recursive popcount-----------*/
long pcount_r (unsigned long x) {
    if (x == 0)
        return 0;
    else
        return (x & 1) + pcount_r(x >> 1);
}

 

 

 

 

 

 

 

 

 

※ processor 상태

실행되는 cpu의 상태정보를 register에 저장한다.

이때, test 결과를 의미하는 code를 Condition code라 부른다.

 

§ Condition Codes

- 모두 1bit로 0이나 1만 저장되며 flag register라고도 불린다.

- Condition code를 setting, 즉 flag register 값을 바꾸는 방법은 다음과 같다.

 

1. 산술연산 [간접 setting] (단, 이때 leaq명령어로는 값이 바뀌지는 않는다.)

- ex)  addq  Src, Dest  ↔  t = a + b

 

 

2. 비교 명령어 [직접 setting]

cmpq  Src2,  Src1   로 표기하며 이때, Src1 - Src2를 계산하여 비교한다.

(단, Src1 - Src2의 결과를 저장하지는 않는다.)

 

 

3. test 명령어 [직접 setting]

testq  Src2,  Src1   로 표기하며 이때, Src1 & Src2 연산을 진행하여 비교한다.

(단, Src1 & Src2의 결과를 저장하지는 않는다.)

 

 

§ Condition Codes값 읽어오기  (feat.  SetX 명령)

§  SetX  ByteReg 

- cmp 명령 실행 후에 적용된다.

- ByteReg의 하위 byte를  condition code (flag register)의  조합 X에 따라  0이나 1로 설정

- 이때, quad와 같은 경우 ByteReg의 나머지 7 byte는 변경이 없다.

즉, %rsp를 예로 들면 최하위 1byte인 %spl을 이용해 0이나 1로 설정한다.

Q. quad를 예로 들면, 그렇다면 1byte를 제외한 나머지 7byte는 어떻게 해야할까?
A. movzbl을 이용해 mov를 이용해 copy 후 나머지 나머지 7byte는 0으로 padding 해준다.

 

Ex. C code  ->  assembly

int gt (long x, long y) { 
    return x > y;
}

 

 

 

 

 

 

 

※ 점프 (jump)

§  jX  Label 

이때, Label은 assembly에서 위치를 나타내며 특정 label로 jump해서 fetch하게 해주는 것이다.

- 조건코드에 따라서 코드의 실행위치를 이동한다.

 

§ 조건부 분기예제  (Old Style)

long absdiff (long x, long y) {
    long result;
    if (x > y)
        result = x - y;
    else
        result = y - x;
    return result;
}

 

 

§ 조건부 분기예제  (Go to 코드 Style)

 

 

§ 조건부 이동명령  (cmovX)

3항 연산자와 같은 한문장으로 표현 가능한 if-else문에서 사용하는데, 위의 go-to보다 더욱 효율적이다.

- 분기문은 pipe라인의 instruction흐름을 매우 방해하지

- 조건부 이동명령은 제어의 이동이 필요가 없기 때문이다.

 

 

§ 조건문 컴파일  (Do-While  loop)

- 바꾸는 방법: do-while => Go to => assembly

이때, x>>=1의 경우, 등호를 통해 shift결과를 x에 save할 수 있게 된 것이다.

 

 

§ 조건문 컴파일 (While  loop)

- 바꾸는 방법: 조건문 => do-while => Go to => assembly

§ 일반적인 While문의 번역- 1

 

 

§ 일반적인 While문의 번역- 2

 

 

 

 

§ 조건문 컴파일  (for  loop)

- 바꾸는 방법: for => while => do-while => go to => assembly

for (init; test; update)
    Body
    

init;
while (test) {
    Body
    update;
}

 

 

 

 

§ 조건문 컴파일  (switch 문)

long switch_eg (long x, long y, long z) {
    long w = 1;
    switch (x) {
        case 1:
            w = y * z;
            break;
        case 2:
            w = y / z;
            /* Fall through */
        case 3:
            w += z;
            break;
        case 5:
        case 6:
            w -= z;
            break;
        default:
            w = 2;
    }
    return w;
}

§ jump table 구조

 

▶ table 구조

- 각 타겟은 8 byte를 필요로 하며  시작주소는 .L4 이다.

 

▶ jump 하기

▷ 직접 점프: jmp  .L8

- 점프 대상은 레이블 .L8로 표시한다.

 

▷ 간접 점프: jmp  *.L4(, %rdi, 8)

- 점프 테이블의 시작: .L4

- 8 byte 주소이기에 8의 배수로 증가해야 함

- 점프 target 유효주소는 .L4 + x * 8 로부터 얻어진다.  (0 ≤ x ≤ 6일 때, 성립)

 

 

(cf. intel의 x86-64 cpu의 assembly를 기준으로 진행.)

※  Assembly란? 

기계어와 1:1 대응관계를 갖는 명령어의 집합체로 low-level 프로그래밍 언어이다.

- 물론 고급언어의 경우, 안전하고 편리하며 컴파일러를 통해 더 훌륭한 어셈블리 프로그램생성이 가능할지도 모른다.

assembly High-level Language
-대형 프로그램 개발이 불편

-속도가 중요한 응용프로그램, 하드웨어 직접 제어 시 이용

-임베디드 시스템의 초기코드 개발 시 이용

-플랫폼에 따라 새롭게 작성해야 해 이식성이 매우 낮음

-하지만 많은 간접적인 응용이 있음

- 배열이나 구조체 없이 메모리에서 연속적 byte들로 표시
-대형 프로그램 개발하기 매우 편함

-이식성이 높음 (high portability)

-비효율적 실행파일의 생성가능성이 존재

-대형 실용 응용프로그램 개발 시 이용

 

 

※  x86 processor

1978년부터 시작되어 점점 새로운 기능들을 추가하였지만, 예전의 기능들을 그대로 유지해 접근이 가능하다.

이런 intel의 철학은 바로 CICS (Complex Instruction Set Computer)로 다양한 형태의 명령어를 갖는다.

- EIP: 다음 명령어를 fetch해오는 주소 program counter

- Register File: Register의 집합

- Condition Code: 가장 최근 연산의 결과의 상태정보를 저장

- Memory: byte주소 지정가능한 배열로 명령어, data가 저장되며 stack이 존재한다.

 

 

※  x86-64의 정수 register

x86-64 processor(CPU)는 64-bit 값을 저장할 수 있는 16개의 register가 존재한다.

이들은 정수 데이터와 포인터를 저장하는 데, 포인터의 경우 주소를 저장하는데 사용합니다.

rsp, rbp와 같이 p로 끝나는 register의 경우가 바로 포인터를 사용해 주소를 저장하는 register이다.

이 16개의 레지스터는 모두 %r로 시작하는 이름을 갖는다.

위 사진을 보면 %rxx 레지스터의 절반의 크기에 %exx라는이름이 붙어있는 것을 확인할 수 있다.

%exx%rxx의 하위 4바이트에 해당하며 이와 같은 방식으로 아래 사진처럼  2바이트, 1바이트에 각각 접근가능!

이때, esp와 ebp는 모두 pointer로 주소를 저장하는 register이다.

이는 예전에 1바이트 단위 접근방식을 남겨두어 예전 의 기능을 그대로 유지해 하위 호완성을 유지하기 위해 과거의 레지스터들의 이름이 남아있는 것이다.

전체 16개의 레지스터의 하위 바이트들은 바이트, 워드(16비트), 더블워드(32비트), 쿼드워드(64비트)씩 접근할 수 있습니다.

 

 

※  데이터의 이동 (Moving Data):

§  movq  Source, Dest

위의 뜻은 Source를 읽어 Destination에 복사를 한다는 의미이다.

 

§  Operand의 유형 

 

▶ Immediate : $로 시작하는 형태,  상수 정수 data

ex) $0x400,  $-503Register : %로 시작하는 형태,  16개 register지만 %rsp는 stack접근에만 이용한다.

ex) %rax,  %r13

Memory : (%register)의 형태, register로 지정되는 주소에 저장된 8개의 연속적 메모리 byte

  - 이때, Memory주소지정을 다음과 같은 방식으로 할 수 있다.

    i) 일반형 (R)  =  Mem[Reg[R]]                  ex) movq (%rcx) , %rax

   ii) 변위형 D(R)  =  Mem[Reg[R] + D]         ex) movq  8(%rbp) , %rdx 

  iii) 가장 일반적인 형태  D(Rb, Ri, S)  =  Mem[Reg[Rb] + S*Reg[Ri] + D] 

       - D: 상수 변위 (1, 2 or 4 bytes)

       - Rb: base register : 16개 register 모두 가능

       - Ri: index register : %rsp를 제외한 모두 가능

       - S: 배율 (1, 2, 4  or 8 bytes)

이때, Dest에는 Immediate 즉, 상수값은 올 수 없다! (상수값에 대입할 수 없어서)

또한, Source와 Dest 모두 Memory인 memory-memory 데이터 이동은 한개의 명령으로 불가능하다.

 

Ex.  아래 함수에 대해 Assembly과정을 이해해보자.

<C code>

void swap (long *xp, long *yp) {
    long t0 = *xp;
    long t1 = *yp;
    *xp = t1;
    *yp = t0;
}

 

<Assembly>

swap:	
    movq	(%rdi), %rax	# t0 = *xp
    movq	(%rsi), %rdx	# t1 = *yp
    movq	%rdx, (%rdi)	# *xp = t1
    movq	%rax, (%rsi)	# *yp = t0
    ret

 

 

 

 

 

※  주소 계산 명령어 (Load Effective Address):

§  leaq  Src, Dst

- Src: 주소 모드 식 (Memory)

- Dst: 수식으로 표현된 주소값 저장 (Register)

 

용도: 메모리를 참조하지 않고 "주소만 계산"할 때  (C에서 &연산자, 곱셈과 같은 경우)

Ex.  아래 함수에 대해 Assembly과정을 이해해보자.

<C code>

long m12 (long x) {
    return x * 12;
}

<Assembly>

leaq (%rdi, %rdi, 2) , %rax		# t <- x + x*2
salq $2,  %rax				# return t << 2

leaq (%rdi, %rdi, 2) , %rax 부분을 통해 %rdi * 3과 같은 결과를 얻고

salq  $2  (shift  arithmetic  left  2자리)연산을 이용해 (3 * %rdi) * 4로 최종적으로 12가 곱해지는 C code와 동일한 결과를 얻는다.

 

 

 

 

 

 

 

+ Recent posts