키워드 "static"은 왜 쓰는것일까? 

 

데니스 리치 & 커니핸은 static의 존재 이유가 같은 프로그램내의 다른 파일들에 있는 똑같은 이름들로 인한 혼란을 방지하기 위함이라고 설명하고 있다.  

 

비야네 스트롭스트룹은 static 키워드를 "use internal linkage"로 해석한다. 

 

결론적으로, 이것은 링킹 과정에서의 에러를 피하기 위함이라고 설명 할 수 있다.

따라서, Linking에 그 원인이 있다고 생각한다.  좀 더 자세한 예제를 통해 살펴보자. 

 

<dog.h>

void sayhello();

<dog.cpp>

#include <iostream>
#include "dog.h"

void sayhello()
{
   std::cout << "Hi, I'm a dog" << std::endl;
}

 

<cat.h>

void sayhello();

<cat.cpp>

#include <iostream>
#include "cat.h"

void sayhello()
{
   std::cout << "Hi, I'm a cat " << std::endl;
}

 

sayhello()가 두 번 정의 및 구현 되었기 때문에 링킹과정에서 에러가 생겨서 빌드를 실패하게 된다. 

dog와 cat 파일의 sayhello()의 translation unit의 범위를 각각의 파일에 한정시킬 필요가 있다.

({dog.h, dog.cpp} | ({cat.h, cat.cpp}) 따라서 static을 쓰는 것이다. 아래와 같이 코드를 수정하면 빌드 할 수 있다.  

 

<dog.h>

static void sayhello();

<dog.cpp>

#include <iostream>
#include "dog.h"

static void sayhello()
{
   std::cout << "Hi, I'm a dog" << std::endl;
}

<cat.h>

static void sayhello();

<cat.cpp>

#include <iostream>
#include "cat.h"

static void sayhello()
{
   std::cout << "Hi, I'm a cat " << std::endl;
}

 

빌드를 성공적으로 수행한다.

이 static 키워드를 선언하면 해당 변수가 파일에서 전역변수로서 사용된다. 다음 예를 보자. 

 

#include <iostream>

static int sayhello()
{
  static int count = 0;
  count++;
  return count;
}

int main()
{
  for (int i = 0; i < 100; i++)
  {
    std::cout << sayhello() << std::endl;
  }
}

주목할 점은, sayhello()를 100번 호출할때마다 static int sayhello() = 0를 100번 수행하여 count를 100번 초기화 하지 않는 다는 것이다 (만약 그랬으면 0,1,0,1,0,1,0,1,0... 이 출력되었을 것이다).

대신, 컴파일 시에 static int sayhello() = 0를 메모리의 데이터영역에 집어 넣고 이후의 선언은 무시하고 count++로 count 값을 증가 시키고 있다 (1,2,3,4,5 ... 97,98,99,100). 

 

참고

 

Brian Kernighan & Dennis Ritchie, <The C Programming Language> 2nd edition

Bjarne Stroustrup, <The C++ Programming Language> 4th edition

'프로그래밍 언어 > C' 카테고리의 다른 글

C - 키워드 extern  (0) 2021.01.24

동시성 프로그래밍은 시스템의 성능을 위해 필수적이다. 

현대의 동시성 프로그램을 구현하는데 아래 세 가지 방법이 사용할 수 있다. 

 

  • 멀티 스레딩 (Multithreading) : 한 개의 프로세스 컨텍스트에서 돌아가는 논리적인 흐름(스레드)를 다중화 시킴.
  • 멀티 프로세싱 (Multiprocessing) : 각각의 프로세스가 각각의 가상주소 공간을 갖으며 (IPC) 메커니즘을 사용함.
  • IO 멀티 플렉싱 (Multiplexing) : 별도의 프로세스를 발생시키지 않고 서버에 도착하는 각기 다른 파일식별자를        이용하여 각기 다른 상태머신으로 전환함.  

참고

 

Randal E Bryant, David R O'Hallaron <Computer Systems : A Programmer's Perspective> 3rd edition

 

open(), read(), write(), close()과 같은 시스템 콜 함수와 fopen(), fread(), fwrite(), fclose() 같은 c언어 표준 함수와 정확히 어떤 차이가 있는지 잘 몰라서 이에 대해 공부 해보기로 하였다. 특히, 이번 포스트에서는 fopen()open()에 대해 다루어 보기로 한다.

 

open()fopen()의 차이는 KLDP 포럼  https://kldp.org/node/1350 에서 그 구체적인 차이와 용도에 대해 시니어 개발자들이 깊게 토론 한 적이 있을 정도로, 겉보기에는 쉬울 것 같지만 깊게 들어가면 심오한 주제라고 생각한다. "Robert Love, 리눅스 커널구조의 심층분석" 을 참고하자면 아래와 같은 구조로 되어있다고 짐작할 수 있다. 

 

fopen() 호출 fopen() 함수 래퍼
C 라이브러리의 함수 래퍼
open()
시스템 콜 
sys_open()
 사용자 공간  커널공간

                                             <그림 1 > "Robert Love, 리눅스 커널구조의 심층분석"에 따른 open() 함수 작동방식 

 

 

이에 대해 자세히 살펴보기 위해 우리가 C/C++에서 자주 쓰는 fopen()의 문법과 용도에 대해서 살펴 보았다. 

 

FILE* fopen(const char* filenameconst char* mode) // 성공 시 해당 파일의 파일 포인터, 실패시 NULL 반환

filename : c 문자열로 된 파일 이름. 

mode : "r" (읽기 전용), "w" (쓰기 전용), "a"(덧붙이기) 등이 있다. 

 

<그림 2> fopen의 사용 예시 fopen_study.c

컴파일이 된다. 어딘가에 fopen() 함수가 선언 및 정의되어 있다고 생각할 수 있다. 

어셈블리 코드를 살펴보면,  fopen함수를 stdio.h 에서 import 하여 사용하고 있다.

 

<그림 3> fopen_study.asm

 

그렇다면 이 fopen() 함수는 어떻게 선언 및 정의 되어 있을까?

우선 fopen()는 Windows10 MSVC 2019 기준으로 <stdio.h>에 함수가 정의 되어있다. 

 

<그림 4> fopen()의 선언 stdio.h

그렇다면 구현코드는 어떻게 씌여져 있을까? API마다 조금씩 다르겠지만, Dennis Ritchie & Brian Kernighan의 저서 "The C Programming Language" 에서 설명하는 fopen() 예제에서 크게 벗어나지 않을 것이라고 생각한다. 그들이 책에 소개한 예제 소스 코드를 살펴보자.  

FILE *fopen(char *name, char *mode) 
{ 
    int fd; 
    FILE *fp; 

    if (*mode != 'r' && *mode != 'w' && *mode != 'a') 
        return NULL; 

    /* Bit operation */ 

    for (fp = _iob; fp < _iob + OPEN_MAX; fp++) 
        if ( (fp->flag & (_READ | _WRITE)) == 0) 
            break;  /* found free slot */ 

    if (fp >= _iob + OPEN_MAX)  /* no free slots */ 
        return NULL; 

    if (*mode == 'w') 
        fd = creat(name, PERMS); 
    else if (*mode == 'a') { 
        if ((fd = open(name, O_WRONLY, 0)) == -1) 
            fd = creat(name, PERMS); 
        lseek(fd, 0L, 2); 
    } else 
        fd = open(name, O_RDONLY, 0); 

    if (fd == -1)   /* couldn't access name */ 
        return NULL; 

    fp->fd = fd; 
    fp->cnt = 0; 
    fp->base = NULL; 
    fp->flag = (*mode == 'r') ? _READ : _WRITE; 
    return fp; 
}

                                                                  <그림 5> fopen()의 정의

 

구현 과정에서 open(), creat(), lseek()와 같은 system call 함수를 사용하고 있음을 알 수 있다.

그렇다면, 이런 시스템 콜은 또 어디에 선언 및 정의되어 있을까? 윈도우 커널소스는 공개되어 있지 않으므로 linux 커널 소스를 통해 궁금증을 해결해 보기로 한다.

 

static __attribute__((unused))
int open(const char *path, int flags, mode_t mode)
{
int ret = sys_open(path, flags, mode);

if (ret < 0) {
SET_ERRNO(-ret);
ret = -1;
}
return ret;
}

<그림 6> 리눅스 커널 tools/include/nolibc/nolibc.h에 정의된 open() 함수의 구현

 

<그림 6>에서 보듯, open은 sys_open()이라는 시스템 콜을 수행하고 있었다. 그렇다면 sys_open()은 어떻게 작동하는가?  sys_open()의 구현부분을 보자. 

static __attribute__((unused))
int sys_open(const char *path, int flags, mode_t mode)
{
#ifdef __NR_openat
	return my_syscall4(__NR_openat, AT_FDCWD, path, flags, mode);
#else
	return my_syscall3(__NR_open, path, flags, mode);
#endif
}

<그림 7> 리눅스 커널 tools/include/nolibc/nolibc.h에 정의된 sys_open() 함수의 구현

 

<그림 7>에서 보듯, 조건에 따라 my_syscall3, 혹은 my_syscall4를 호출하고 있다.  그들이 어떻게 작동하는지 보자.

#define my_syscall3(num, arg1, arg2, arg3)                                    \
({                                                                            \
	register long _num asm("r7") = (num);                                 \
	register long _arg1 asm("r0") = (long)(arg1);                         \
	register long _arg2 asm("r1") = (long)(arg2);                         \
	register long _arg3 asm("r2") = (long)(arg3);                         \
									      \
	asm volatile (                                                        \
		"svc #0\n"                                                    \
		: "=r"(_arg1)                                                 \
		: "r"(_arg1), "r"(_arg2), "r"(_arg3),                         \
		  "r"(_num)                                                   \
		: "memory", "cc", "lr"                                        \
	);                                                                    \
	_arg1;                                                                \
})

#define my_syscall4(num, arg1, arg2, arg3, arg4)                              \
({                                                                            \
	register long _num asm("r7") = (num);                                 \
	register long _arg1 asm("r0") = (long)(arg1);                         \
	register long _arg2 asm("r1") = (long)(arg2);                         \
	register long _arg3 asm("r2") = (long)(arg3);                         \
	register long _arg4 asm("r3") = (long)(arg4);                         \
									      \
	asm volatile (                                                        \
		"svc #0\n"                                                    \
		: "=r"(_arg1)                                                 \
		: "r"(_arg1), "r"(_arg2), "r"(_arg3), "r"(_arg4),             \
		  "r"(_num)                                                   \
		: "memory", "cc", "lr"                                        \
	);                                                                    \
	_arg1;                                                                \
})

<그림 8> 리눅스 커널 tools/include/nolibc/nolibc.h에 정의된 my_syscall3,  my_syscall4

 

확인 결과, my_syscall3, my_syscall4는 인라인 어셈블리로 쓰여져 있다.  

쓰다보니 글이 너무 길어졌다. 인라인 어셈블리 부터는 다음 포스트에 연재하기로 한다. 

 

Robert Love, Linux Kernel Development

Randal E Bryant, David R O'Hallaron <Computer Systems : A Programmer's Perspective> 3rd edition

Dennis Ritchie & Brian Kernighan <The C Programming Language>

'컴퓨터 시스템 > IO' 카테고리의 다른 글

Unix 시스템 입출력 1 - 개요  (0) 2021.01.20

시스템 입출력 : 메모리와 외부장치 (디스크 드라이브, 터미널, 네트워크) 간에 데이터를 복사하는 과정.  

 

리눅스에서 모든것은 파일 ("Everything is a file.")로 간주되고, 디바이스들 또한 파일로 모델링 된다.

 

파일 

- 파일은 연속된 바이트들의 집합이다. 사용자가 디바이스에 접근하고자 하면, 커널은 File Descriptor 값을 반환한다.

- 디바이스, 소켓 등도 파일이다. 

- 운영체제는 다루는 파일이 이진파일인지 텍스트 파일인지 알지 못한다. 

 

디렉토리

 

- 디렉토리는 링크들의 집합이다.

- 점 한개 = (자신의 디렉토리), 점 두개 (부모의 디렉토리)

 

유닉스 운영체제 에서 File Descriptor는 files_struct 구조체의 *fd_array[] 배열의 인덱스 번호이며 inode를 가르키고 있다.  open() 함수를 이용하여 값을 받으며, 실패시 -1를 받는다. 표준입출력과 표준에러를 위해 고정적으로 쓰이는 File Descriptor 값은 아래와 같다. 

 

식별자 의미 Descriptor 값
STDIN_FILENO 표준입력 0
STDOUT_FILENO 표준출력 1
STDERR_FILENO 표준에러 2

 

유닉스 시스템 수준의 open(), read(), write(), close() 등을 사용하여 파일 입출력을 할 수 있다.

read()는 open() 으로 받은 File Descriptor 인자 값을 이용하여 파일 읽기 작업을 수행한다.

write()도 마찬가지로 File Descriptor 값을 사용한다.  close() 할때 인자 값으로 File Descriptor 값을 넣어주어야 한다. 

 

이런 시스템 콜을 사용하여 fopen(), fclose() 등과 같은 C 표준 라이브러리 함수를 구현한 것이다.    

이에 대해서는 다음 포스트에서 서술해 보기로 한다. 

 

참고

 

임성락,  리눅스 시스템 프로그래밍 

Randal E Bryant, David R O'Hallaron <Computer Systems : A Programmer's Perspective> 3rd edition

'컴퓨터 시스템 > IO' 카테고리의 다른 글

Unix 시스템 입출력 2 - open() 의 작동원리  (0) 2021.01.22

Intel x86 Architecture 범용 레지스터

 

데이터 레지스터

 

EAX Accumulator Register 산술연산 (덧셈, 곱셈, 나눗셈), 함수의 반환 값 저장함

EBX Base Register 일반적인 레지스터, 주소 인덱싱에 사용됨. 

ECX Count Register 반복 되는 갯수를 세는 용도로 사용됨

EDX Dobule-Precision Register 입출력에 사용됨. 큰 수의 나눗셈때 EAX를 도와 연산을 수행함

 

인덱스 레지스터

 

ESI Source Index Pointer Data 출발지의 주소가 저장됨

EDI Destination Index Pointer 목적지의 주소가 저장됨

 

포인터 레지스터

 

EBP Base Pointer Register 스택의 처음 부분을 가르키는 역할

ESP Stack Pointer Register 스택의 끝 부분을 가르키는 역할

EIP  Instuction Pointer Register 다음 명령어를 가르키는 역할

 

참고

 

이재광, 전병찬 IBM PC 어셈블러 프로그래밍

"Professor Hank Stalica" 님의 유튜브 채널 www.youtube.com/channel/UC-RZhAum87am1bsFAJ_HV-g

'프로그래밍 언어 > Assembly' 카테고리의 다른 글

명령어 push, pop  (0) 2021.03.05
명령어 mov, lea  (0) 2021.03.04

MSVC에서 자주 사용하는 단축키를 정리하는 문서 이다. 이 중 다수의 단축키는 MSVC IDE에 국한 되지 않고 다른 IDE, NotePad, 편집기, 웹 문서 등에서 공통적으로 사용된다. 지속적으로 추가 업데이트 하려고 한다. 

 

Home (한 번 누르면) 커서가 있는 줄에서 첫 번째 "문자"로 커서 이동 (두 번 누르면) 커서가 있는줄에서 맨 앞으로 이동.

End 커서가 있는 줄에서 맨 마지막 문자로 이동

 

F5 디버깅

F7 해당 파일만 컴파일

Shift + Ctrl + b 빌드

 

Ctrl + Home 현재 문서 맨 위로 이동 

Ctrl + End   현재 문서 맨 아래로 이동

Ctrl + 다음 띄어쓰기로 오른쪽 이동

Ctrl + 다음 띄어쓰기로 왼쪽 이동

Ctrl + del  단어 단위로 삭제

Ctrl + g  해당 라인(줄)으로 이동

Ctrl + h    솔루션 전체에서 문자를 replace 하는 단축키

Ctrl + f     찾기

Ctrl + x     undo

Ctrl + y     redo

 

Ctrl + Space 자동 완성 기능 

Ctrl + Alt + l 해당 커서 줄 삭제

 

Shift + 오른쪽으로 한 문자씩 드래그 (Shift + Ctrl + -> 시 단어 단위로 오른쪽으로 드래그)

Shift + 왼쪽으로 한 문자씩 드래그 (Shift + Ctrl + <- 시 단어 단위로 오른쪽으로 드래그)

Shift + ↑ 위쪽으로 한 줄씩 이동하면서 그 줄에 있단 모든 문자가 드래그 

Shift + ↓ 아래쪽으로 한 줄씩 이동하면서 그 줄에 있단 모든 문자가 드래그  

Shift + Alt + (→, ←, ↑, ) 해당 방향으로 이동하며 블록 지정

 

 

 

링킹(linking)은 컴파일된 오브젝트 파일 (.obj)을 모아서 하나의 실행 파일을 만드는 과정이다. 요즘 시대의 IDE(MSVC, Xcode 등)에는 빌드시에 자동적으로 링킹이 수행되지만, 과거에는 컴파일 후에 수동으로 링킹을 해주었다.  다음과 같이 크게 두 종류의 링킹이 있다.

 

Static Linking

 

- 컴파일시에 코드를 링킹하는 방법이다. 

- 재배치 가능 목적파일들을 연결하는 과정이다. 

- 링킹시 연결할 목적코드당 코드를 중복하여 복사 붙여 넣기하기 때문에 실행파일의 크고,

  컴파일에 소요되는 시간이 증가하지만, 프로그램 실행시 동적 링킹보다는 빠르게 기능을 수행한다. 

- 프로그램 빌드에 필수적인 정적 파일은 이미 포함되어 있지만, 이외의 추가적인 파일은 별도로 설정 해주어야한다. 

  예 ) MSVC의 경우, 빌드 전에 프로젝트 속성 > 링커 > 일반 > 추가 라이브러리 디렉터리 //  경로추가

                                   프로젝트 속성 > 링커 > 입력 > 추가 종속성 // lib 파일 추가

  

Dynamic Linking

 

- 런타임시에 코드를 링킹하는 방법이다.

- 컴파일시 소요되는 시간이 줄어들고 실행파일 크기가 작지만,

  프로그램 실행시 정적 링킹보다는 느리게 기능을 수행한다. 

- 필요한 .dll 파일을 실행파일과 같은 경로에 두면 된다. 

 

 

참고

 

Randal E Bryant, David R O'Hallaron <Computer Systems : A Programmer's Perspective> 3rd edition

docs.microsoft.com

MSVC 2019로 여러가지 형변환을 해보았다. RTTI 형식 정보사용은 on을 해두었다.

 

Implicit Type Conversion (암시적 형 변환)

 

(예시1)

unsigned short a = 2;

int b = a; 

// a - b = 0

 

그러나, 다음과 같은 상황에서는 값이 잘릴 수 있다.

 

(예시2)

int a = 925;

unsigned char b = a;  // max 값이 255인 unsigned char에 925를 대입해보자.

// a - b = 768 의도하지 않은 값이 나온다.

 

(예시3)

double a = 0.123456789123456789;  // 배정도(double-precision)의 double를,
float b = a;                                // 단정도(single-precision)의 float에 대입.

// a - b  = -1.91982e-09

 

Explicit Type Converion (명시적 형 변환)

 

1. cast (C)

 

C언어에서 사용하는 강제 형변환 방식

수식을 잠시 원하는 형으로 변환시켜준다 (단, 자체는 변하지 않음). 

문법 : (형 이름) 수식 

 

(예시4)

float a = 1 / 3; // a  = 0

float a = (float) 1 / 3  // a = 0.33333

float a = 1 / (float) 3  // a = 0.33333

float a = (float) 1 / (float) 3 // a = 0.33333

 

2. static_cast (C++)

 

C++에서 컴파일타임에 수행되는 형변환 방식

C 방식의 casting과 차이라면, 컴파일 타임에 타입 검사를 해준다. 

문법 : static_cast<형 이름>(수식)

주로 numeric value를 변환시킬때 사용한다.

 

(예시5)

float a = static_cast<float>(1) / 3; // a = 0.33333

 

포인터 변수 형 변환에 사용될수 있지만, 컴파일 타임에만 타입을 체크하고 런타임에는 하지 않기 때문에 Microsoft에서는 이것을 추천 하지 않는다고 명시해두었다.예를 들어, static_cast로 다운캐스팅을 강제하여 사용하면 문제가 생길 수 있다. 

 

(예시6)

parent* p = new parent();
child* c = static_cast<child*>(p);  // Down Casting. Not Safe 런타임때 에러가 안 걸린다.

그러므로 잘못된 값을 그대로 쓰게 된다. 

 

3. dynamic_cast (C++)

 

(예시7)

C++에서 런타임에 수행되는 형변환 방식

문법 : dynamic_cast<형 이름>(수식)

numeric value를 변환시킬 수 없다.

 

(예시7)

float a = static_cast<float>(1) / 3; // 컴파일 에러

 

안정성 때문에 포인터 변수 형 변환에 staitc_cast 대신 사용된다.

 

(예시8)

parent* p = new parent(); 
child* c = static_cast<child*>(p);  // Down Casting. Not Safe 런타임때 에러가 걸린다. 

 

4. reinterpret_cast (C++)

 

2의 static_cast와 3의 dynamic_cast가 같은 (자료형 <-> 다른 자료형), (포인터 <-> 포인터) 끼리만 해주었다면, 

reinterpret_cast는 숫자 -> 주소값으로도 가능하다. 

<The C++ Programming Language>에 수록된 예제를 살펴보면,

IO_device *d1 = reinterpret_cast<IO_device*>(0xff00); // 저런식으로 raw memory 접근을 시도한다. 

컴파일러는 당연히 컴파일될때 0xff00의 값이 타당한지 체크를 하지않는다. 따라서 reinterpret_cast 매우 위험한 캐스팅이다. 스트롭스트룹는 이 형변환을 "for changing the meaning of bit patterns"라고 정의하였다. 

 

5. const_cast (C++)

 

const로 선언된 변수에 접근하여 값을 수정할 권한이 생기는 형변환이다.

 

10에서 9로 바뀌었다. 

 

참고

 

Brian Kernighan & Dennis Ritchie, <The C Programming Language> 2nd edition

Bjarne Stroustrup, <The C++ Programming Language> 4th edition

docs.microsoft.com

+ Recent posts