5. Software 비네팅(Vinetting)
목차
- ⓐ Context와 AAPCS
- ⓑ Pointer와 Array는 소녀시대와 원더걸스 , 그리고 이중포인터
- ⓒ struct와 typedef 그리고 PACKED
- ⓓ STACK, HEAP에 관한 소고.
- ⓔ Stack의 정체와 자세히 보기 - initialization까지
- ⓕ 함수가 불렸을 때 일어나는 일. - Stack 뒤지기 신공
- ⓖ Stack 동작의 비밀과 실제 메모리 덤프
- ⓗ Stack Size는 어떻게 잡는가
- ⓘ 함수 포인터와 실행주소 변경
- ⓙ Linked List와 Queue
ⓐ Context와 AAPCS
- Context
- 현재 CPU에 대한 모든 정보. 즉 레지스터들의 현재 값들
- 시스템이 동작하는데 필요한 모든 정보를 담고 있음
- R0~R12는 계산용
- R13 : Stack의 Top부분. 마지막으로 push한 데이터의 주소
- R14 : Linked Register. 어디서 왔는지, 어디로 돌아가야하는지
- PC : 현재 실행하고 있는 주소 정보
- CPSR : 현재 CPU의 상태에 대한 주소
- Callee(호출된 함수)에서 Register 값을 변경하면 Caller 함수에서 Register 값이 변경되어 문제가 발생함
- 이를 방지하기 위해, Callee는 Caller로 복귀할 때, 원래의 환경을 그대로 복구해주어야함
- Scratch Register(R0-R3, R12)는 Callee가 마음대로 변경 및 수정, 훼손의 권리를 가지고 있음. 어느정도의 유연성
- Function Call의 경우, Scratch Register를 훼손하였으나, 다른 함수를 호출하지 않는 경우(Leaf 함수) Stack에 백업할 필요헚이, 돌아올 떄,
MOV pc, lr
을 이용해 곧바로 복구 가능
- Exception 발생 순간, Function Call, Scheduler에 의한 Context Switching 모두 비슷함
- RTOS에서의 Context Switching은 추후 다룰 예정
ⓑ Pointer와 Array는 소녀시대와 원더걸스 , 그리고 이중포인터
- Pointer : 주소를 가리키는 word 크기의 자료 형
int *addres
- pointer 형 자료형, address 자체의 크기는 32bit, address가 가리키는 자료형은 integer type
&
:실제 메모리 주소를 확인하는 연산자.
- EX1)
char *text = "Recipes;
text
는 문자 ‘R’이 저장되어있는 주소를 가리킴- text가 0x100이라면, 0x101은 e, 0x102는 c …
text[1];
‘e’,*(text+1);
도 ‘e’를 가리킨다- 이는 컴파일러가 똑같이 해석하기 때문. Sugar Expression이라고 함
- 개발자가 어떻게 하더라도 같은걸 가리킨다는 의미
array[번호]
는*(array주소 + 번호)
와 같음- Ex2)
1
2
3
4
5
6
7
8
char *pointer;
char array[3];
array[0] = 0;
array[1] = 1;
array[2] = 2;
pointer = array;
pointer = &array[0];
- 마지막 두 문장은 같은 의미
- 왜 포인터를 사용하는가? 포인터 사용의 장점
- 직접 주소를 가리킬 수 있기 때문에, Assembly와 유사함
- High Level Language에서 각종 memory 주소에 Access할 수 있는것이 장점
- Function Call을 할 때, Passing Argument로 주소를 직접 전달 가능
- 대용량의 데이터의 전달을 간단히 주소 하나로 가능하게 해줌.
- EX3) swap
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int process(void)
{
int a, b;
int a = 1;
int b = 2;
swap(&a, &b);
}
void swap(int* a, int* b)
{
int tmp;
tmp = *a;
*a = *b;
*b = tmp;
return;
}
- process함수는 a와 b의 주소를 swap 함수에 넘겨주고, swap 내부에서 알아서 주소를 가져다 값을 바꿈
- 이중 포인터 Pointer를 가리키는 Pointer
- 컴파일러에 따라 컴파일이 될수도 안될수도 있음
- pointer 배열에서 가장 많이 사용됨
- EX4) 이중 포인터
1
2
3
4
5
6
7
8
9
10
11
12
void gettag (char *ptag)
{
ptag=(char *)malloc(40);
strncpy(ptag,"pointer tag", sizeof(char)*40);
}
void process()
{
char *tag;
gettag(tag);
free(tag);
}
- tag는 init되지 않은 char pointer
- gettage의 Passing Param으로 전달됨
- ptag에 heap을 할당받아 그 시작 주소를 넣음
1
2
3
4
5
6
7
8
9
void gettag (char ptag){
ptag = 10;
return;
}
void process(){
char tag;
gettag(tag);
}
- 다음과 같이 설정한 경우,
process()::tag
의 값이 제대로 설정되지 않는다!
1
2
3
4
5
6
7
8
9
10
11
12
void gettag (char **ptag)
{
*ptag=(char *)malloc(40);
strncpy(*ptag,"pointer tag", sizeof(char)*40);
}
void process()
{
char *tag;
gettag(&tag);
free(tag);
}
- 다음과 같이 2중 포인터를 설정해야 tag값을 제대로 저장할 수 있음
ⓒ struct와 typedef 그리고 PACKED
Struct
struct
키워드는 구조체를 가진 변수를 만드는 키워드
1
2
3
4
5
6
7
8
struct customer{
char name;
int height;
int weight;
}
customer kim[100];
customer *kim;
- customer 구조체의 크기는? 1byte + 4byte + 4byte = 9byte?
- 실제로는 내부에서 실제 자리 크기와는 상관 없이 4byte 단위로 alignment함
- 12byte를 사용함
__packed
키워드를 사용하여 byte alignment 할 수 있음- 이를 이용해 9byte로 줄일 수 있음
- Embedded System과 PC Host가 USB 통신을 할 때, PC는 int를 4byte로 인식하고, Embedded system에서는 2byte로 인식한다면, 서로 같은 데이터라도 막상 packet 형태로는 다르게 인식되는 문제 발생
- 이런 문제를 해결하기 위해 Data의 byte alignment는 byte로 하기로 결정.
- 물론 메모리 효율도 좋음
- GNU에서는
__attribute__((packed))
로 표현함
Typedef
struct
키워드를 깔끔하게 쓰기 위해typdef
를 사용함
1
2
3
4
5
6
typedef unsigned char byte;
typedef unsigned char unit8;
typedef unsigned short word;
typedef unsigned long dword;
char a; byte a; unit8 a; // 모두 같음
- 자신이 보기 편한 data형으로 이름을 바꿀 수 있다
1
2
3
4
5
6
7
8
typedef struct customer{
char *name;
int height;
int weight;
} customer_type;
customer_type lee;
customer_type *dukgu;
customer_type
을 데이터형으로 재정의함- 보통은
customer_type
등과 같이 type의 의미를 덧붙여 헷갈림을 방지하여 명명함 - 재귀적으로 자기가 자기 모양을 갖는 member를 갖게 하고 싶은 경우(linked list등 노드가 필요한 경우)
1
2
3
4
5
6
typedef struct heap_node{
char filename [SIZ_FILE_NAME];
unsigned int line_number;
void *allocated;
struct head_node *next;
} heap_node_type;
- 여러가지 경우를 가지는 case를 구현할 때 사용함
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef enum{
START,
WALK,
RUN
} customer_activity_type;
customer_activity_type activity;
switch(activity){
case START:
~~~
break;
case WALK:
~~~
break;
case RUN:
~~~
break;
}
ⓓ STACK, HEAP에 관한 소고
STACK
- 자료구조 stack이 아닌 메모리 영역의 STACK에 대한 설명
- tcc를 통해 compile 하게 되면, 함수 호출 시 어떤식으로 구현되는지,
stack이 메모리에 어떻게 자리잡는지, option은 어디있는지, 실제로 stack이 어떤식으로 이용되는지는
“함수의 구조와 함수가 불렸을 때 일어나는 일”에서 다룰 예정
Heap
- Stack과 유사하나, 동적 할당에 사용함
- 특히 Linked list, Tree 구조 등에 많이 사용함
- 동적 할당 : 가변적인 양만큼의 data를 처리하기 위해 사용하는 할당 방법
- 필요한 만큼만 꺼내 쓴다
alloc
,free
를 통해 메모리를 빌려오고, 반납한다
1
2
3
4
void HEAP(int n){
int *p;
p = (int *)malloc((sizeof(int))*n);
}
- integer size를 n개만큼 연속적인 Memory 영역을 heap에서 가져온다.
*p
를 통해 첫번째 원소에 접근 가능. 배열처럼 사용 가능- Fragmentation(조각화, 파편화) 문제 발생.
- 메모리를 사용후 반납하지 않으면 메모리 누수 발생!
- 특히, pointer를 local로 선언한 다음, free해주지 않으면, 그 함수가 끝나 return 되어버려서, 돌려줄 방법도 없어져버림
ⓔ Stack의 정체와 자세히 보기 - initialization까지
- 임베디드 시스템에서는 Heap, Stack 모두 전역변수의 배열로 선언되어있음
- 메모리 상에 일단 어떤 영역을 확보한 후, 이 영역에 대해 어떻게 처리할 것이냐에 의해 Stack인지 Heap인지를 구분함
- Stack이나 Heap도 Bootup시, 내가 원하는대로 여러가지 종류로 선언하여 사용할 수 있음
- 보통 Stack은 높은 주소에서 낮은 주소로, Heap은 낮은 주소에서 높은 주소로 쌓아감
- 자신의 시스템이 stack이나 heap이 어떻게 데이터를 쌓는지 알아두어야, Stack Back Tracking 이 가능
stack recipe_stack[200000]
이라면, stack중 가장 늦게 쌓이게 되는 stack limit은recipe_stack[0]
, 가장 먼저 쌓이는 곳은recipe_stack[199999]
가 될것임- 스택 구현의 4가지 방법
- ascending은 높은 주소로 자라는 방법, decending은 낮은 주소로 자라는 방법
- 현재 stack pointer가 방금 push, pop한 데이터를 포함하면 Full, 아니라면 empty로 구분
- Stack pointer가 데이터를 넣은 후 변하느냐 아니면 먼저 변하느냐에 따라 After, Before로 구분함
- Stack은 Multiple register transfer addressing 명령어를 이용해 push, pop을 함
- ST/LD : store, load
- M : multiple
- IB/IA/DB/DA : Increase Before/After, Decrease Before/After
- FA/FD/EA/ED : Full Ascending/Decending, Empty Ascending/Decending
- ARM state와 Thumb state에서의 stack의 명령어가 다름
ⓕ 함수가 불렸을 때 일어나는 일. - Stack 뒤지기 신공
- 함수를 호출했을 때 일어나는 일
1
2
3
4
5
6
7
8
9
10
A() {
B();
★
}
B() { C(); }
C() { D(); }
D() { ... }
- 다음과 같은 함수가 있을 때, A() -> B() -> C() -> D() 순으로 호출함
- A() 입장에서는 ★ 위치로 복귀해야함
- 이를 stack에 복귀할 위치를 저장해 두어 복귀함
- 서브 루틴 호출 시 수행되는 일
- 전달 인자와 돌아갈 주소를 스택에 Push하는 일
- 함수 호출(즉, pc를 불려진 함수의 주소로 jump 시킴)
- 지역 변수에 대해 스택에 저장공간을 할당
- 호출된 함수를 수행함
- stack에서 할당된 지역변수 저장 공간의 해제
- 돌아갈 주소를 stack으로부터 꺼내와서 함수로부터의 복귀
- 전달인자에 의해 사용되었던 공간을 해제
ⓖ Stack 동작의 비밀과 실제 메모리 덤프
- 예시를 통해 Stack 분석
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
N _ R0 FFFE R8 0
Z Z R1 0 R9 0
C C R2 5074 R10 0
V _ R3 1E6C1A35 R11 0
I _ R4 0 R12 0
F _ R5 0 R13 **1F6E92C8**
T T R6 FFFF R14 1E6C1A1D
J _ R7 1F6E943C PC 1E6C19BC
usr SPSR CPSR 60000030
Q _
A _ USR: FIQ:
E _ R8 0 R8 0
R9 0 R9 C5400100
0 _ R10 0 R10 1F62C9F0
1 _ R11 0 R11 F00898F0
2 _ R12 0 R12 DD
3 _ R13 1F6E92C8 R13 1
R14 1E6C1A1D R14 F0008BFC
SPSR 10
SVC: IRQ:
R13 E000A880 R13 F008CD00
R14 F0000000 R14 1DB55BF8
SPSR 60000010 SPSR 60000010
UND: ABT:
R13 60000010 R13 00100000
R14 1DB55BF8 R14 1EFCE4BE
SPSR 60000010 SPSR 20000030
- CPSR : User Mode, Thumb mode로 실행중. IRQ나 FIQ는 enable
- PC는 1E6C19BC를 가리킴
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
addr/line__|code_____|label____|mnemonic________________|comment________
|
|word b_funct(word arg, word param)
3958|{
ST:1E6C19B8|B570 b_funct: push {r4-r6,r14}
ST:1E6C19BA|B0B2 sub sp,#0xC8
| int loop;
3960| word ret = 0;
ST:1E6C19BC|2400 mov r4,#0x0 // PC는 여기를 가리킴
| word array[100];
|
3963| for (loop=0; loop < 100; loop++)
ST:1E6C19BE|2200 mov r2,#0x0
ST:1E6C19C0|466E cpy r6,r13
| {
3965| if (loop%2)
ST:1E6C19C2|07D3 lsl r3,r2,#0x1F ; r3,loop,#31
ST:1E6C19C4|D002 beq 0x1E6C19CC
3966| array[loop] = arg;
word b_funct(word, arg, word param)
함수를 실행 중- Stack Pointer를 통해 어떤 함수가 얘를 호출했는지 확인 가능
- 1F6E92C8 에서 호출
- 이런식으로 Stack을 거꾸로 확인할 수 있다면, 소프트웨어의 동작 과정을 추적할 수 있음
ⓗ Stack Size는 어떻게 잡는가
- 실무에서 사용하는 Stack 측정 방법
- Stack의 값을 최대로 많이 사용 했을 때의 최대 값을 사용하고
- 그 최대 크기를 전체 Stack크기의 3/4으로 잡아 System을 구성함.
- ex) Stack의 시작 주소가 0x1000이고, 가장 많이 push해서 쌓았을 때 주소가 0x400이라면,
- 함수의 이름은 Symbol. 물리적인 주소를 점유함
- 실제 함수의 이름이 뜻하는 것은 실행 코드 영역에서의 함수의 시작 주소를 의미함
- 함수 이름 자체를 어딘가의 포인터에 연결하여, 포인터가 가리키는 곳을 실행 시키면 함수를 실행시킬 수 있음
1. 함수 포인터의 선언
자료형 (* 함수포인터이름)(인자목록)
- 사칙연산 함수를 넣는 예시
- 함수 포인터는 디바이스 드라이버들을 그때그때 다르게 쓰고 싶을 때 사용하면 좋음
- 주변 Device들 중에서 같은 기능을 하지만, 여러가지 Vendor의 Device들을 한꺼번에 지원하고 싶을 때
- 어떤 device가 read, write 기능을 한다고 가정했을 때, 아래와 같은 포인터 함수 정의
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct {
const char *name;
void (*read) (byte *buffer, int count);
void(*write) (byte *buffer, int count);
} device_type;
device_type device =
{
"It's me",
device_read,
device_write
}
device_type *device_target;
device_target = &device
- 이렇게 해주면 사용자 입장에서는
(*device_target->read)(buffer, count)
로 호출 가능 typedef int (function)(void)
- function이라는 것이, int return값을 받으며, void 인자를 받는 함수
function *temp = hello;
- 임베디드 시스템 개발 중, 특정 주소로 branch가 필요한 경우가 있음
1
2
3
void (*example) (void);
example = (void (*)())0x7777;
(*example)();
- 위와 같이 억지로 pc를 0x7777로 만들어주는 효과
- 혹은
(*(void(*)())0x7777)();
와 같이 한줄로 구현 가능
ⓙ Linked List와 Queue
- Queue
- FIFO
- IPC에 많이 사용되므로, 잘 알아둘 것
- put, get을 사용함
- 구현 방법이 다양함. array, linked list, 등
- front, rear를 통해서 slide window 형식으로 valid한 data를 모아놓고, data를 넣을 때 rear쪽에, data를 뺄 때는 front쪽에서
- overflow를 방지하기 위해 Ring buffer를 사용하기도 함
- Linked List
- 서로 다른 데이터 공간에 있어도 pointer를 통해 연결되는 자료 구조
- Linked List를 이용한 Queue의 구현이 RTOS Kernel에 많이 사용됨
- Free Queue를 사용
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.