ls -al 명령어를 실행후 hint가 존재 하길래 cat hint로 열어보았다 .


"텍스트 파일 편집 중 쉘의 명령을 실행시킬 수 있다는데..." 라고 힌트가 적혀있다

여기서 먼저 텍스트 파일  편집을 하는 법을 알아봐야겠다. 

트레이너에서 배운 cat > 명령어가 잇지만 나는 vi 편집기를 사용하기로 하였다.


level1과 마찬가지로 level3로 넘어가는 권한이 걸린것을 찾아보았다

/usr/bin/editor 에 권한이 존재한다고 나오고있다 

editor라고 하는거보니 굳이 편집기를 내가 사용하지 않아도 자체적으로 editor 에서  실행될수도 잇을거같다.


/usr/bin/editor로 이동을하니 역시나 vi편집기가 실행이된다.


너무나도 간단하다. vi 편집기에서 FTZ 워게임의 다음레벨 비밀번호를 얻기 위한 바이너리인 my - pass를 실행시키니 비밀번호가 나왔다.


'해커스쿨 ftz' 카테고리의 다른 글

FTZ- LEVEL6  (0) 2016.04.10
FTZ - LEVEL5  (0) 2016.04.10
FTZ - LEVEL4  (0) 2016.04.10
FTZ - LEVEL3  (0) 2016.04.10
FTZ - LEVEL1  (0) 2016.04.10


ls -al 명령어를 이용하여 숨겨진 파일과 디렉토리를 출력하였다


cat 명령어를 이용하여 hint를 열어봤더니

level 2 권한에 setuid가 걸린 파일을 찾으라고 한다 .

그럼 level2 권한이 걸린 setuid를 찾아야겠다


2>/dev/null은 2번 스크립터(에러스크립트)를 안나오게 해주는 명령어다 

이렇게 level2 권한이 걸린 setuid를 찾으니 /bin/ExcuteMe에 존재한다고한다 

bin/ExecuteMe로 이동한 화면이다 

참고로 여기서 my-pass는 ftz 워게임 상에서 다음 레벨로 이동하기 위한 바이너리다 

쉘이라던가 다른 바이너리로 실행시켜 level2의 권한으로 명령어를 실행하면 된다

쉘을 실행시킨후 id 명령어를 사용하였다 유저의 권한이 level2로 변경된것을 볼 수 있다

이 후에 my-pass 바이너리를 실행시키면 level2의 비밀번호가 등장한다 .


'해커스쿨 ftz' 카테고리의 다른 글

FTZ- LEVEL6  (0) 2016.04.10
FTZ - LEVEL5  (0) 2016.04.10
FTZ - LEVEL4  (0) 2016.04.10
FTZ - LEVEL3  (0) 2016.04.10
FTZ LEVEL2  (0) 2016.04.10


1. 프로그램 수행 관련 명령


  $ gdb -q [파일명]

    => gdb를 조용히(?) 켠다


  $ run [args] 

    => 디버깅할 프로그램의 수행 시작


  $ set args [args]

    => 프로그램이 시작 될 때, 전달할 인자 설정


  $ next [n]

    => breakpoint를 걸은 프로그램에서 다음 n(default=1)개의 문장을 수행하고 멈춘다.

    => 함수인 경우 함수 전체를 수행합니다.


  $ step [n] 

    => next [n]과 비슷합니다.

    => 차이점으로, 함수의 경우 함수에 들어가 한 문장씩 수행합니다.


  $ continue [n]

    => Ctrl+C이나 breakpoint에 멈추어진 프로그램의 수행을 계속합니다.

    => 여기서 n이 지정될 경우 n번째 breakpoint에서 수행을 멈추게 됩니다.



2. 브레이크 포인트 


  $ watch [EXP]

    => expression EXP 에 대한 watchpoint를 설정합니다.

    => EXP의 값이 변할 때마다 프로그램의 수행을 멈춥니다.


  $ delete [N]

    => breakpoint를 제거합니다. 인자값이 없으면 모든 bp를 제거합니다.

    => delete breakpoint 1 ( 예시 )


  $ enable [N .. ]

    => breakpoint를 enable 시킵니다. N은 breakpoint 숫자입니다.



3. 상태 확인

 


  $ info breakpoints [N]

    => 설정된 breakpoint 상태를 보여줍니다.


  $ info args

    => 현재 스택 프레임의 인자 변수를 보여줍니다.


  $ info display

    => 프로그램이 멈출 때 출력할 expression을 보여줍니다.




4. 이 외의 명령어


  $ quit

    => gdb를 종료합니다.


  $ make [args]

    => make를 실행합니다.


  $ set disassembly-flavor intel

    => intel 형식으로 어셈블리를 봅니다. ( set dis int 줄임 가능 )


  $ disassembly [func_name]

    => 함수의 어셈구문 코드를 확인합니다.


  $ print [$register or func_name]

    => 스택에 저장된 값을 보여줍니다.


  $ x/x [$register]

    => 해당 레지스터에 들어있는 값을 Hex로 표현


  $ x/5s [$register]

    => 해당 레지스터에 들어있는 값을 String으로 5개까지 표현


  $ x/d [$register]

    => Decimal


  $ x/i [$register]

    => Instructions


  $ list

    => Source File이 어딨는지 보여줌


  $ where

    => 어디에서 실행되었는지 보여줌


  $ main info sec

    => 실행가능한 섹션들을 보여줍니다.



'시스템' 카테고리의 다른 글

dummy 크기 정확히 파악하기 - 출처 http://beist.org  (0) 2016.05.01
코어덤프  (0) 2016.05.01
공유메모리  (0) 2016.02.18
시스템 $0x80  (0) 2016.02.11
프로시저와 스택 구조  (0) 2016.02.10

1 공유메모리 (shared memory)

보통 프로세스에서 사용되는 메모리영역은 해당 프로세스만이사용할수 있다. 하지만 때때로 여러개의 프로세스가 특정 메모리영역을 사용했으면 하는때가 있을것이다.

System V IPC 설비중의 하나인 "공유메모리"를 통해서 이러한일을 할수있다.

1.1 개요

모든 프로세스(:12)는 자신의 업무를 수행하기 위해서 필요한 자료를 저장하기 위한 메모리 공간을 가지게 된다. 이러한 메모리공간에는 CPU에 의해 수행되는 명령어들, 프로그램 시작시 정의되고 초기화된 데이타, 프로그램 시작시 정의되었지만 초기화 되지 않은 데이타, 함수호출에 필요한 정보, 동적할당이 이루어지는 데이타등 이 들어가게 된다. hjh 프로세스는 시작시 혹은 실행중에 이러한 데이타를 저장하고 사용하기 위한 메모리 공간을 커널에 요구하여서 할당받아 사용하게 되는데, 이러한 메모리공간은 기본적으로 메모리를 요청한 프로세스만이 접근가능하도록 되어있다. 하지만 가끔은 여러개의 프로세스가 특정 메모리 공간을 동시에 접근해야할 필요성을 가질때가 있을것이다.

공유메모리는 이러한 작업을 위한 효율적인 방법을 제공한다.

공유메모리는 여러 IPC 중에서 가장 빠른 수행속도를 보여준다.

그이유는 하나의 메모리를 공유해서 접근하게 되므로, 데이타 복사와 같은 불필요한 오버헤드가 발생하지 않기 때문으로, 빠른 데이타의 이용이 가능하다. 그러나 하나의 프로세스가 메모리에 접근중에 있을때, 또다른 프로세스가 메모리에 접근하는 일이 발생하면 자칫 데이타가 홰손될수 있을것이므로, 한번에 하나의 프로세스가 메모리에 접근하고 있다는걸 보증해줄수 있어야 할것이다.

이러한 작업을 위해서 Unix(:12) 에서는 Semaphore(:12) 라는 또다른 공유자원을 제어할수 있도록 해주는 도구를 제공해준다. 이번 문서에서는 Semaphore 를 다루지는 않을것이다. 이것은 다른 문서에서 다루도록 하고 여기에서는 단지 공유메모리에 대해서만 다루도록 할것이다.

다음은 공유메모리에 관련된 함수들의 모음이다.
#include <sys/types.h>
#include <sys/shm.h>

int shmget(key_t key, int size, int shmflg)
void *shmat( int shmid, const void *shmaddr, int shmflg )
int shmdt( const void *shmaddr)
int shmctl(int shmid, int cmd, struct shmid_ds *buf)


=== 공유메모리는 어떻게 할당되는가 == 위의 함수들을 설명하기 전에 우선 공유메모리가 어떻게 할당되고, 어떤 과정을 통해서 접근가능한지에 대해서 우선 알아보도록 하겠다.

공유메모리의 생성요청은 최초 공유메모리 영역을 만드는 프로세스가 커널에 공유메모리 공간의 할당을 요청함으로써 이루어지며, 만들어진 공유메모리는 커널에 의해서 관리 되게 된다.

이런 이유로 한번만들어진 공유메모리는 운영체제를 리부팅하거나, 직접 공유메모리 공간을 삭제시켜주지 않은한, 공유메모리를 사용하는 모든 프로세스가 없어졌다고 하더라도, 계속적으로 유지되게 된다.

프로세스가 커널에게 공유메모리 공간을 요청하게 되면, 커널은 공유메모리 공간을 할당시켜주고 이들 공유메모리공간을 관리하기 위한 내부자료구조를 통하여, 이들 공유메모리를 관리하게 된다. 이 자료는 shmid_ds 라는 구조체에 의해서 관리되며 shm.h 에 정의되어 있다.
struct shmid_ds
{
    struct         ipc_perm shm_perm;    // 퍼미션
    int            shm_segsz;            // 메모리 공간의 크기
    time_t         shm_dtime;            // 마지막 attach 시간 
    time_t         shm_dtime;            // 마지막 detach 시간 
    time_t         shm_ctime;            // 마지막 변경 시간
    unsigned short shm_cpid;             // 생성프로세스의 pid
    unsigned short shm_lpid;             // 마지막으로 작동한 프로세스의 pid
    short          shm_nattch;           // 현재 접근한 프로세스의 수
};
Unix 버젼에 따라서 멤버변수들이 약간씩 차이를 보일수 있다.
  • shm_perm 공유메모리는 여러개의 프로세스가 동시에 접근 가능하므로, 파일과 같이 그 접근권한을 분명히 명시해줘야 한다.
  • shm_segsz 할당된 메모리의 byte 크기이다
  • shm_atime 가장최근의 프로세스가 세그먼트를 attach한 시간
  • shm_dtime 가장최근의 프로세스가 세그먼트를 detach한 시간
  • shm_ctime 마지막으로 이 구조체가 변경된 시간
  • shm_cpid 이 구조체를 생성한 프로세스의 pid
  • shm_lpid 마지막으로 작동을 수행한 프로세스의 pid
  • shm_nattch 현재 접근중인 프로세스의 수
이러한 공유메모리에 접근을 하기 위해서는 고유의 공유메모리 key 를 통해서 접근가능해지며, 이 key값을 통해서 다른 여러개의 공유메모리들과 구분되어 질수 있다.


1.2 shmget

shmget 은 커널에 공유메모리 공간을 요청하기 위해 호출하는 시스템 호출 함수이다. key 는 바로 위에서 설명했듯이 고유의 공유메모리임을 알려주기 위해서 사용된다. shmget 을 이용해서 새로운 공유메모리 영역을 생성하거나 기존에 만들어져있던 공유메모리 영역을 참조할수 있다 




  첫번째 아규먼트는 여러개의 공유메모리중 원하는 공유메모리에 접근하기 위한 Key 값이다. 이 Key 값은 커널에 의해서 관리되며, Key 값을 통해서 선택적인 공유메모리에의 접근이 가능하다.    두번째 아규먼트는 공유메모리 의 최소크기 이다. 새로운 공유메모리를 생성하고자 한다면 크기를 명시해주어야 한다.  존재하는 메모리를 참조한다면 크기는 0으로 명시한다.   3번째 아규먼트는 공유메모리의 접근권한과, 생성방식을 명시하기 위해서 사용한다. 아규먼트의 생성방식을 지정하기 위해서 IPC_CREAT 와 IPC_EXCL 을 사용할수 있다. 아래 이들에 대해서 설명을 해두었다. 




  * IPC_CREAT
key 를 이용 새로운 공유메모리 공간을 만든다.
  • IPC_EXCL
    IPC_CREAT와 같이 사용되며, 공유메모리 공간이 이미 존재할경우 error 를 되돌려준다.

    만약 IPC_CREAT 만 사용된다면 shmget()은 새로 생성되는 공유메모리공간을 지시하는 공유메모리공간 "식별자" 되돌려준다. 만약 입력된 key 값이 지시하는 공유메모리 공간이 이미 존재하고 있다면 존재하는 공유메모리 공간의 "식별자"를 되돌려준다. IPC_EXCL 과 IPC_CREAT 를 같이 사용할경우, 공유메모리 공간이 존재하지 않으면 새로 생성시켜주며, 존재할경우에 error 를 되돌려준다.

  • 3번째 아규먼트는 이외에도 권한을 지정해줄수도 있다. 권한은 파일권한과 동일하게, 유저, 그룹, Other 에 대한 읽기/쓰기 권한을 지정할수 있다. 단 실행권한은 줄수 없도록 되어 있다. 아래와 같이 사용가능하다.
    int shmid;
    key_t keyval;
    
    keyval = 1234;
    shmid = shmget(keyval, 1024, IPC_CREAT | 0666)); 
    if (shmid == -1)
    {
        return -1;
    }
    

    1.3 shmat

    일단 공유메모리 공간을 생성했으면, 우리는 공유메모리에 접근할수 있는 int 형의 "식별자" 를 얻게 된다. 우리는 이 식별자를 shmat 를 이용해서 지금의 프로세스가 공유메모리를 사용가능하도록 "덧붙임" 작업을 해주어야 한다.

    첫번째 아규먼트는 shmget을 이용해서 얻어낸 식별자 번호이며, 두번째 아규먼트는 메모리가 붙을 주소를 명시하기 위해 사용하는데, 0을 사용할경우 커널이 메모리가 붙을 주소를 명시하게 된다. 특별한 사항이 없다면 0을 사용하도록 한다. 세번째 아규먼트를 이용해서, 우리는 해당 공유메모리에 대한 "읽기전용", "읽기/쓰기가능" 모드로 열수 있는데, SHM_RDONLY를 지정할경우 읽기 전용으로, 아무값도 지정하지 않을경우 "읽기/쓰기 가능" 모드로 열리게 된다.

    1.4 shmdt

    프로세스가 더이상 공유메모리를 사용할필요가 없을경우 프로세스와 공유메모리를 분리 하기 위해서 사용한다. 이 함수를 호출할 경우 단지 현재 프로세스와 공유메모리를 분리시킬뿐이지, 공유메모리 내용을 삭제하지는 않는다는 점을 기억해야 한다. 공유메모리를 커널상에서 삭제 시키길 원한다면 shmctl 같은 함수를 이용해야 한다.

    shmdt 가 성공적으로 수행되면 커널은 shmid_ds 의 내용을 갱신한다. 즉 shm_dtime, shm_lpid, shm_nattch 등의 내용을 갱신하는데, shm_dtime 는 가장 최근에 dettach (즉 shmdt 를 사용한)된 시간, shm_lpid 는 호출한 프로세세의 PID, shm_nattch 는 현재 공유메모리를 사용하는 (shmat 를 이용해서 공유메모리에 붙어있는) 프로세스의 수를 돌려준다. shmdt 를 사용하게 되면 shm_nattch 는 1 감소하게 될것이며, shm_nattch 가 0 즉 더이상 붙어있는 프로세스가 없다라는 뜻이 될것이다. shm_nattch 가 0이 되어있을때 만약 이 공유메모리가 shm_ctl 등에 의해 삭제표시 가 되어 있다면, 이 공유메모리는 삭제되게 된다.

    1.5 shmctl

    이것은 공유메모리를 제어하기 위해서 사용한다. 즉 shmid_ds 를 직접 제어함으로써, 해당 공유메모리에 대한 소유자, 그룹 등의 허가권을 변경하거나, 공유메모리를 삭제혹은, 공유메모리의 잠금을 설정하거나 해제하는 등의 작업을 한다.

    2번째 아규먼트를 이용해서 shmid 가 가르키는 공유메모리를 제어하며, cmd 를 이용해서 원하는 제어를 할수 있다. cmd 를 이용해 내릴수 있는 명령에는 다음과 같은 것들이 있다.
    • IPC_STAT 공유메모리 공간에 관한 정보를 가져오기 위해서 사용된다. 정보는 buf 에 저장된다.
    • IPC_SET 공유메모리 공간에 대한 사용자권한 변경을 위해서 사용된다. 사용자 권한 변경을 위해서는 슈퍼유저 혹은 사용자권한을 가지고 있어야 한다.
    • IPC_RMID 공유메모리 공간을 삭제하기 위해서 사용된다. 이 명령을 사용한다고 해서 곧바로 사용되는건 아니며, 더이상 공유메모리 공간을 사용하는 프로세스가 없을때, 즉 shm_nattch 가 0일때 까지 기다렸다가 삭제된다. 즉 해당 공유메모리 공간에 대해서 삭제표시를 하는거라고 생각하면 된다.

    다음은 실제로 공유메모리를 사용하는 방법에 대한 가장간단한 예제이다. 자식과 부모프로세스간에 어떻게 메모리가 공유되는지 보여준다.

    예제 : shm.c
    #include <sys/ipc.h> 
    #include <sys/shm.h> 
    #include <string.h> 
    #include <unistd.h> 
    
    
    int main()
    {
        int shmid;
        int pid;
    
        int *cal_num;
        void *shared_memory = (void *)0;
    
    
        // 공유메모리 공간을 만든다.
        shmid = shmget((key_t)1234, sizeof(int), 0666|IPC_CREAT);
    
        if (shmid == -1)
        {
            perror("shmget failed : ");
            exit(0);
        }
    
        // 공유메모리를 사용하기 위해 프로세스메모리에 붙인다. 
        shared_memory = shmat(shmid, (void *)0, 0);
        if (shared_memory == (void *)-1)
        {
            perror("shmat failed : ");
            exit(0);
        }
    
        cal_num = (int *)shared_memory;
        pid = fork();
        if (pid == 0)
        {
            shmid = shmget((key_t)1234, sizeof(int), 0);
            if (shmid == -1)
            {
                perror("shmget failed : ");
                exit(0);
            }
            shared_memory = shmat(shmid, (void *)0, 0666|IPC_CREAT);
            if (shared_memory == (void *)-1)
            {
                perror("shmat failed : ");
                exit(0);
            }
            cal_num = (int *)shared_memory;
            *cal_num = 1;
    
            while(1)
            {
                *cal_num = *cal_num + 1;
                printf("child %d\n", *cal_num); 
                sleep(1);
            }
        }
    
        // 부모 프로세스로 공유메모리의 내용을 보여준다. 
        else if(pid > 0)
        {
            while(1)
            {
                sleep(1);
                printf("%d\n", *cal_num);
            }
        }
    }
    
    예제 프로그램이 하는 일은 간단하다. int 형의 공유메모리 공간을 할당한다음. 자식프로세스에서 여기에 1씩을 더하고 부모프로세스에서는 공유메모리 내용을 출력하는 일을한다.

    2 공유메모리 제어하기

    쉘에서 공유메모리의 상황을 보여주기 위해서 ipcs(1)란 도구를 제공한다. ipcs 를 사용하면 공유메모리 뿐만 아닌, Semaphore, Message Queue 등 소위 sytem V IPC 설비에 대한 내용을 보여준다. 그리고 ipcrm 도구를 이용해서 필요없는 공유메모리, Message Queue, Semaphore 등을 지워줄수 있다.

    위의 예제코드를 컴파일 시켜서 실행시킨다음 ipcs 를 이용해서 확인을 해보면 공유메모리 자원이 어떤식으로 관리되는지 좀더 이해를 쉽게 할수 있을것이다.

    2.1 공유 메모리 정보 확인

    -l 옵션과 함께 ipcs를 실행하면 ipc(:12)자원 제한 정보를 확인할 수 있다.
    $ ipcs -l
    ------ Shared Memory Limits --------
    max number of segments = 4096
    max seg size (kbytes) = 32768
    max total shared memory (kbytes) = 8388608
    min seg size (bytes) = 1
    
    ------ Semaphore Limits --------
    max number of arrays = 128
    max semaphores per array = 250
    max semaphores system wide = 32000
    max ops per semop call = 32
    semaphore max value = 32767
    
    ------ Messages: Limits --------
    max queues system wide = 1706
    max size of message (bytes) = 8192
    default max size of queue (bytes) = 16384
    
    -m 옵션으로 실행하면 현재 사용중인 ipc 자원 정보를 확인할 수 있다.
    $ ipcs -m
    
    ------ Shared Memory Segments --------
    key        shmid      owner      perms      bytes      nattch     status      
    0x00000000 0          root       777        135168     2                       
    0x00000000 819201     yundream   600        393216     2          dest         
    0x00000000 950274     yundream   600        393216     2          dest         
    0x00000000 983043     yundream   600        393216     2          dest         
    0x00000000 917508     yundream   600        393216     2          dest         
    0x00000000 1015813    yundream   600        393216     2          dest         
    0x00000000 1048582    yundream   600        393216     2          dest         
    0x00000000 27590663   yundream   600        393216     2          dest         
    0x00000000 35684360   yundream   666        4343780    2          dest         
    0x00000000 35717129   yundream   666        282808     2          dest  
    

    2.2 /proc 파일 시스템으로 제어하기

    리눅스 운영체제(:12)는 /proc 파일 시스템으로 공유 메모리 자원 값을 변경할 수 있다.
    • /proc/sys/kernel/shmmax : 프로세스가 생성할 수 있는 공유 메모리의 최대 크기
    • /proc/sys/kernel/shmall : 현재 사용중인 공유 메모리 크기
    프로세스가 생성할 수 있는 공유 메모리 크기는 다음과 같이 변경하면 된다.
    // 500M로
    # echo 536870912 > /proc/sys/kernel/shmmax
    

    출처  : :http://www.joinc.co.kr/modules/moniwiki/wiki.php/Site/system_programing/IPC/SharedMemory

    '시스템' 카테고리의 다른 글

    dummy 크기 정확히 파악하기 - 출처 http://beist.org  (0) 2016.05.01
    코어덤프  (0) 2016.05.01
    리눅스 gdb 명령어 (계속 추가)  (0) 2016.02.28
    시스템 $0x80  (0) 2016.02.11
    프로시저와 스택 구조  (0) 2016.02.10

    int $0x80은 운영체제에 할당된 인터럽트 영역으로 system call을 하라는 뜻이다

    '시스템' 카테고리의 다른 글

    dummy 크기 정확히 파악하기 - 출처 http://beist.org  (0) 2016.05.01
    코어덤프  (0) 2016.05.01
    리눅스 gdb 명령어 (계속 추가)  (0) 2016.02.28
    공유메모리  (0) 2016.02.18
    프로시저와 스택 구조  (0) 2016.02.10

    2.1 16진수와 2진수


    컴퓨터 프로그래밍의 원리적인 이해를 위해 2진법과 16진법에 대해 알아 본다. 16진수(hexadecimal)는 0~9의 숫자와, a~f의 6개의 문자를 사용한다. C 언어에서는 16진수의 상수를 표현할 때, 10진수의 상수표현과 구분하기 위해, 상수 앞에 0x를 붙인다.

    2진수

    (binary)
    16진수(hexadecimal)
    10진수(decimal)

    0000
    0
    0

    0001
    1
    1

    0010
    2
    2

    0011
    3
    3

    0100
    4
    4

    0101
    5
    5

    0110
    6
    6

    0111
    7
    7

    1000
    8
    8

    1001
    9
    9

    1010
    a
    10

    1011
    b
    11

    1100
    c
    12

    1101
    d
    13

    1110
    e
    14

    1111
    f
    15




    위 표에서 알 수 있듯이, 16진수의 1자리는 2진수의 4자리 즉 4비트로 표현되는 것을 알 수 있다. 따라서 16진수와 2진수는 서로 상호 변환이 쉽게 이루어진다. 예를 들어 8비트 2진수 10110110이란 수는 이를 하위의 4비트씩 그룹으로 묶으면, 1011과 0110의 2그룹으로 나눌 수 있다. 그리고 각각의 그룹에 위의 변환표를 적용한다. 즉 1011은 16진수에서 b이고, 0110은 16진수에서 6이다. 따라서 10110110은 16진수로 b6가 된다. 이제 왜 위의 표를 암기해야 하는지 알 수 있을 것이다. 아무리 16비트, 32비트 등 8비트 이상의 많은 비트의 2진수도 이와 같은 방법으로 16진수 간단히 변환될 수 있다. C 언어방식대로, 16진수 앞에는 0x를 붙여 표기한다. 따라서 10110110의 16진수 표기는 0xb6이 된다. 이번에는 반대로 16진수를 2진수로 바꾸어 본다. 예를 들어 0x8a라는 16진수를 2진수로 변환해 본다. 이 때, 0x8a에서 접두어 0x를 빼고, 8a에 대해, 각각의 16진수 한자리씩 위의 변환표를 이용하여, 2진수로 변환한다. 8은 1000, a는 1010이므로, 이 2개의 2진수 1000과 1010을 붙여 쓰면, 10001010의 8비트 2진수 표현이 된다.



    2.2 비트와 바이트


    컴퓨터는 하드웨어적으로 수많은 스위치들의 결합으로 이루어진다. 스위치는 회로적 연결상태(ON)와 회로적 단락상태(OFF)의 2가지 논리적 상태(logical state)로 동작한다. ON상태가 논리적인 1을 의미하며, OFF상태가 논리적 0을 의미한다. 엄밀히 말하면, 하나의 스위치도 컴퓨터라고 말할 수 있다. 다만 이 컴퓨터는 0 또는 1값만을 저장할 수 있는 하나의 메모리만 가지고 있을 뿐이다. 동작 또한 단지 그 값을 0으로 또는 1로 바꾸는 메모리 쓰기 동작과 현재의 상태를 알려주는 읽기 동작 밖에 할 수 없는 것이다. 단 하나의 스위치 이지만, 여기서 중요한 정의를 내릴 수 있다. 첫째, 메모리라는 정적기능와 메모리 읽기 쓰기라는 동적기능이 그것이다. 여러분에 C를 설명하기 전에 컴퓨터의 구조에 대해 설명하고자 한다. 이의 이해를 통해 보다 체계적으로 컴퓨터 메모리 구조와 연산원리를 이해할 수 있으리라 기대된다. 이 하나의 스위치를 비트(bit)라 한다. 이 하나의 비트로 저장할 수 있는 데이터는 0~1범위의 데이터를 단 하나 저장할 수 있을 뿐이다. 이제 이를 좀 더 확장하여, 이 비트가 2개 있다고 가정한다. 이 경우 2가지 선택이 있을 수 있다. 먼저 이를 각각 메모리로 활용하면, 즉 서로 다른 데이터로 구분하면, 2가지 데이터를 기억할 수 있다. 각각 하나의 데이터는 0~1의 범위만을 가질 뿐이다. 이 경우를 2개의 1 비트 데이터 저장용량을 갖는다고 말할 수 있다. 다른 방법으로는 이 2개의 비트를 하나로 묶어 하나의 데이터로 만드는 경우이다. 이 때 2개의 비트가 가질 수 있는 경우의 수는 4가지로 늘어난다. 이 두개의 비트를 b0와 b1이라 할 때, 그 경우의 수는 다음 표와 같다



    표 2 2비트의 표현

    b1
    b0
    2진수
    10진수

    0
    0
    00
    0

    0
    1
    01
    1

    1
    0
    10
    2

    1
    1
    11
    3




    이 표로부터 2비트 데이터로 표현할 수 있는 수의 범위는 10진수로 0~3인 것을 알 수 있다. 그리고 또한 스위치 하나의 추가가 2진법에서 자리 수의 증가를 의미하는 것을 알 수 있다. 따라서 만약 스위치가 8개로 늘어나고, 이 8개의 스위치가 하나의 데이터를 표현한다면, 이 8개의 비트가 표현할 수 있는 경우의 수는 256이 된다. 따라서 8비트 데이터가 표현할 수 있는 수의 범위는 10진수로 0~255인 것을 알 수 있다. 이 8개의 비트(bit)를 묶은 것을 바이트(byte)라 한다. 실제 컴퓨터에서는 바이트 단위로 데이터가 저장된다.



    2.3 메모리 용량과 주소


    이제 좀더 컴퓨터의 스위치의 개념을 확장하여, 2,048개의 비트를 갖는 컴퓨터를 가정한다. 1,024개 비트의 용량을 1K라 할 때, 이 컴퓨터는 2K 메모리 용량을 갖는다. 이를 바이트 단위로 환산하면, 256바이트의 메모리 용량을 갖는다. 다시 말해, 0~255까지의 수를 표현할 수 있는 바이트 데이터를 256개의 종류로 저장할 수 있다. 컴퓨터는 데이터를 구분하기 위해 메모리 주소(memory address)를 사용한다. 이제 이 256개의 메모리에 0에서 255까지의 번호를 붙여 데이터를 구분할 수 있다. 이 번호를 메모리 주소라 한다. 메모리 주소는 좀 더 많은 수를 표현하기 위해 16진법을 사용하여 표현한다.

    이제 다시 메모리 이야기로 되돌아 가자. 2,048개의 비트를 갖는 메모리는 256바이트의 메모리 용량을 갖는다고 하였다. 이 때 이 메모리의 주소는 0번지에서 255번지를 갖게 되는데, 이를 16진수로 표현하면, 0x0번지에서 0xff번지의 주소를 갖는다라고 말한다. 이제 비트 수를 늘려나가 보자. 이제 주소를 기준으로 메모리의 용량을 계산하여 보자. 만약 메모리 주소가 0xffff의 4자리의 16진법으로 표현될 수 있다면, 이 주소번지로 구분할 수 있는 메모리 용량은 256*256=65,536개의 바이트가 된다. 이를 우리는 줄여서 64K라 한다. 컴퓨터 역사와 비교해 볼 때, 1980년대 초 애플컴퓨터 시대의 메모리 용량이 이러하였다. 이제 메모리주소의 16진법의 자리수와 메모리 크기와의 관계를 아래의 표에 정리해 본다.



    표 3 메모리 주소와 메모리 용량

    메모리 주소 범위
    메모리 용량(bytes)
    표기(bytes)
    시기/컴퓨터

    0~0xf
    16



    0~0xff
    256



    0~0x3ff
    1,024
    1K


    0~0xfff
    4,096
    4K


    0~0xffff
    65,536
    64K
    1980's/ Apple

    0~0xfffff
    1,048,576
    1,024K or 1M
    1984/IBM-XT

    0~0xffffff
    16,777,216
    16M
    1990's/IBM-AT

    0~0xfffffff
    268,435,456
    256M
    2000's/Pentium

    0~0xffffffff
    4,294,967,295
    4,096M or 4G
    near future




    이러한 메모리 주소 범위와 메모리 용량을 10진수로 표현하는 프로그램을 다음과 같이 작성한다.



    리스트 1 메모리 용량

    1: #include



    2: void main()

    3: {

    4: printf("%x=%u\n",0xffff,0xffff);

    5: printf("%x=%u\n",0xfffff,0xfffff);

    6: printf("%x=%u\n",0xffffff,0xffffff);

    7: printf("%x=%u\n",0xfffffff,0xfffffff);

    8: printf("%x=%u\n",0xffffffff,0xffffffff);

    9: }



    이 프로그램의 실행결과는 여러분이 직접 확인해 보기 바란다.



    2.4 데이터형과 메모리할당


    이 절에서는 데이터형(data type)에 대해, 알아본다. 프로그램에서 데이터는 변수(variables)와 상수(constants)로 나뉘어 진다. 변수는 사용되기 전에 데이터형이 선언(declaration)되어야 한다. 데이터형을 좀 더 쉽게 설명하기 위해, 가장 작은 크기의 데이터형인 자료형인 char에 대한 설명부터 시작한다. 먼저 다음의 프로그램을 보자.



    리스트 2 변수의 메모리

    1: #include

    2: void main()

    3: {

    4: unsigned char a;

    5: printf("%x\n",&a);

    6: }



    리스트 2의 프로그램은 실제 코드는 4번과 5번의 단 2줄이지만, 이 코드에는 메모리의 개념을 이해하기 위한 많은 내용이 포함되어 있다. 먼저 이 프로그램의 최종 목적은, 8비트 변수 a를 하나 선언하고, 이 변수에 할당되어 있는 메모리 주소 값을 화면에 출력하는 것이다. 그런데 여기서 알아 두어야 할 것은, 변수에 대한 정의이다. 변수는 메모리의 특정 바이트를 이용해서 값을 갖는다. 이 프로그램과 같이 unsigned char형 변수는 1바이트의 메모리를 이용하여, -128~127까지의 정수를 표현한다. 자세한 변수의 데이터형에 대해서는 나중에 좀 더 자세히 알아 보기로 한다. 중요한 점은 이 변수에 대한 메모리 주소 할당을 사용자가 직접 할 수 없다는 사실이다.

    하드웨어 프로그래밍, 즉 운영체제의 간섭을 받지 않는 낮은 수준의 하드웨어 제어용 시스템 프로그램에서는 사용자가 임의로 변수에 메모리의 주소를 지정할 수 있다. 그러나 운영체제하의 컴퓨터에서는 이러한 직접 메모리 접근(access) 금지되어 있다. 만약 이를 허용한다면, 기본적인 운영체제의 틀이 흔들리기 때문이다. 즉 운영체제는 모든 컴퓨터자원의 관리의 책임을 맡기 때문에, 사용자의 직접 메모리사용은 허용하지 않는 것이다.

    리스트 2의 프로그램에서 4번줄은 unsigned char형 정수 변수 a을 선언하는 내용이다. 그러나 이는 소프트웨어 프로그래밍 측면에서 보면 다소 관념적 표현이다. 보다 하드웨어적인 면에서의 변수 a의 선언에 대한 정확한 표현은 다음과 같다. 즉 현재의 메모리(물론 그 용량은 컴퓨터 자원에 따라 달라진다) 중, unsigned char형 정수로 사용 가능한 메모리 1바이트를 할당하여, 그 할당된 주소의 메모리로 표현되는 값을 앞으로 a라고 쓰겠다고 선언한다. 메모리 1바이트는 32비트 운영체제에서는 4바이트의 주소(address)로 식별되며, 그 주소에는 1바이트의 데이타(data)가 기억된다.

    char라는 자료형은 이 1바이트의 데이터를 8비트 정수로 사용하겠다는 데이터 형태를 규정하는 것이다. 따라서 컴파일러는 이러한 문장을 만나면, 프로그램이 요구하는 메모리의 크기와 현재 유용 가능한 메모리 영역을 고려하여, 적당한 메모리주소를 변수 a에 할당한다. 현재의 예에서는 변수 a는 8비트 정수형의 데이터를 위한 것이므로, 컴파일러는 1바이트 크기의 메모리를 변수에 할당한다. 앞서 말했듯이, 이 주소 할당의 메카니즘은 컴파일러와 운영체제의 소관이므로, 프로그램에게는 개방되어 있지 않다. 따라서 어떤 주소의 메모리가 할당될 지는 할당되기 전에는 프로그래머가 예측할 수 없다. C 프로그램에서 메모리 할당(memory allocation)은 문법적 오류(syntax error)를 체크하는 컴파일(compile) 단계가 완료된 이후에, 링크(link and relocation)단계에서 수행된다. 링크단계를 거쳐야 비로소 실행가능한 파일(executable file)이 만들어지는데, 이 단계에서는 실제 특정변수가 어떤 메모리번지에 할당되었는지 알 수가 있다. 리스트 2의 5번줄이 이러한 변수의 할당된 주소를 알아보는 문장(statement)이다. 5번줄의 &는 변수에 할당된 주소를 알려주는 번지연산자(reference operator)이다. 그리고 5번줄의 %x는 printf()함수에서 해당 변수를 16진수 포맷으로 출력하라는 스트링이다. 이 프로그램의 실행결과는 여러분이 직접 알아보라.


    이 결과를 통해, 변수a는 0x12ff7c라는 메모리 번지에 할당되어 있는 것을 알 수 있을 것이다. 리스트 2의 내용을 확장하여, 여러 가지 다른 데이터형의 변수들에 대해, 어떻게 메모리가 할당되는 지 알아본다. 먼저, 변수 a와 같은 데이터형의 변수 b를 추가로 선언하고, 역시 변수 b의 할당된 주소를 알아본다. 다음은 이를 위한 프로그램이다.



    리스트 3 추가된 변수의 메모리 할당

    1: #include

    2: void main()

    3: {

    4: unsigned char a,b

    5: printf("%x\n",&a);

    6: printf("%x\n",&b);

    7: }



    이 프로그램에서 추가된 것은 4번줄에서, 변수 b를 unsigned char형 정수로 추가로 선언한 것과 6번줄의 변수 b의 할당된 메모리 주소를 화면에 출력하는 print()문이다. 이 프로그램을 실행 또한 여러분이 직접 해보시기를 바란다.


    이 결과에서 변수a에 0x12ff7c번지의 메모리를, 변수 b에 0x12ff78번지의 메모리를 할당한 것을 알 수 있다. 변수a와 변수b의 크기기 각각 1바이트인 것을 고려하면, 이러한 메모리할당은 의외이다. 실제 1바이트 변수 2개밖에 이 프로그램에서는 사용되지 않았는데, 실제 할당된 주소는 연속적이지 않고, 4의 차이가 나는 것을 알 수 있다. 즉 메모리 할당이 비경제적인 것을 알 수 있다. 이로부터 단일변수의 선언은 비록 그 변수의 크기가 1바이트라 하더라도 4바이트의 간격으로 메모리 공간을 할당받는다는 사실을 알 수 있다. 이를 확인하기 위해, 리스트13-2의 프로그램에서, 변수 a와 b를 각각 16비트 정수형(short int), 32비트 정수형(long int), 32비트 실수형(float) 등으로 선언하여 보아도, 같은 간격의 메모리 주소 할당 결과를 갖는 것을 알 수 있다. 물론 64비트 변수인 64비트 실수형(double) 변수에 대해서는 다른 결과를 갖는다. 이를 확인하기 위해, 리스트13-2를 다음과 같이 변경하여 작성한다.



    리스트 4 64비트 실수형의 메모리주소 할당

    1: #include

    2: void main()

    3:{

    4: double a,b;

    5: printf("%x\n",&a);

    6: printf("%x\n",&b);

    7: }



    리스트 4이 리스트 3와 다른 것은 4번줄의 자료형이 double로 바뀐 것이다. C프로그램에서 double형은 64비트 부동소수점 실수형이다. 이 데이터형에 대해서는 다음에 좀 더 설명한다. 이 프로그램의 실행은 여러분이 직접해보시길 바란다. 이 결과로 부터 64비트형 변수의 주소할당은 정확히 8바이트씩 차이가 나는 것을 알 수 있다. 아 사실로부터 다음과 같은 결론을 내릴 수 있다.



    (1 (1) 단일 변수는 변수의 크기가 32비트이하인 경우, 변수의 크기에 상관없이 4바이트 크기의 메모리를 할당받는다.
    (2) 단일 변수의 크기가 64비트인 경우, 8바이트 크기의 메모리를 할당받는다.



    그럼 실제 1바이트크기의 변수에 1바이트 크기의 메모리를, 2바이트 크기의 변수에 2바이트 크기의 메모리를 할당받기 위한 방법은 무엇일까? 그 방법은 변수를 배열로 선언하는 것이다. 배열변수란 동일한 데이터형을 갖는 복수개의 요소를 갖는 변수이다. 배열변수는 선언할 때, 변수명 뒤에 []를 붙이고, []안에 사용할 배열요소의 개수를 지정한다. 그리고 수식에서 이 변수가 사용될 때, 배열 변수 뒤에 []를 붙이고, []안에 사용할 배열요소의 번호를 지정한다. 배열요소의 번호는 0부터 시작한다. 예를 들어 10개의 배열요소를 갖는 배열변수a를 정수형으로 선언한다고 하면, 다음과 같이 선언한다.



    int a[10];



    여기서 a[0]는 배열변수 a의 첫번째 요소이고, a[9]이 배열변수 a의 마지막 요소가 된다. 이러한 배열변수는 벡터(vector)나 행렬(matrix)와 같이 복수개의 같은 데이터형의 데이터를 표현하고자 할 때 주로 사용된다. 배열변수에 대해서는 나중에 좀 더 자세히 다루기로 한다. 이 절에서는 배열변수로 선언된 변수의 메모리 할당에 대해서만 알아 보자. 다음은 이를 위한 프로그램 예이다.



    리스트 5 배열변수의 메모리 할당(1)

    1: #include

    2: void main()

    3: {

    4: unsigned char a[10];

    5: printf("%x\n",&a[0]);

    6: printf("%x\n",&a[1]);

    7: }



    리스트 5의 4번줄에서 unsigned char형 정수 변수로 10개의 요소를 갖는 배열변수 a[10]을 선언하고, 5번줄에서, 그 변수들의 할당된 주소를 알아본다. 이 프로그램을 여러분이 실행시켜보라.
    이 결과로부터 배열변수 a[0]과 배열변수 a[1]에 각각, 0x12ff74와 0x12ff75의 메모리가 주소가 할당된 것을 알 수 있다. 즉 두개의 변수가 연속적인 주소로 할당된 것을 알 수 있다. 그리고 또한, 배열변수의 요소번호가 증가할 때마다, 그 배열요소에 할당된 메모리주소도 증가하는 것을 알 수 있다. 이를 좀 더 확인하기위해, 다음의 프로그램을 작성해 본다.



    리스트 6 배열변수의 메모리할당(2)

    1: #include

    2: void main()

    3: {

    4: unsigned char a[10];

    5: for (char i=0;i<10;i++)

    6: printf("a[%d]'s adress=%x\n",i, &a[i]);

    7: }



    리스트 6에서 5번줄에서 for()을 사용하여, 6번줄의 문장을 10번 반복시킨다. 그리고 반복시 마다 i=0에서 i=9까지 증가시켜나간다. 이러한 for()문에 대해서는 나중에 다시 설명하기로 한다. 여기서는 일단 위와 같이 반복시키기 위해 사용된다고만 알아두자. 그리고 6번줄의 print()문을 통해 a[0]에서 a[9]까지 각각의 변수에 할당된 메모리 주소를 알아본다. 이 프로그램을 실행시켜 보라.

    이 결과를 통해, 다시 한번 알 수 있는 것은 배열변수는 연속적으로 메모리주소가 할당되며, 배열번호가 증가함에 따라, 할당된 주소도 선언된 데이터형의 크기 간격으로 증가함을 알 수 있다. 만일 배열변수가 16비트 정수형으로 선언되면, 할당된 메모리 주소는 2의 간격을 가지고 연속적으로 할당될 것이다. 다음은 이를 확인하기 위한 프로그램이다.



    리스트 7 16비트 정수형 배열변수의 메모리 할당

    1: #include

    2: void main()

    3: {

    4: unsigned short a[10];

    5: for (char i=0;i<10;i++)

    6: printf("a[%d]'s address=%x\n",i, &a[i]);

    7: }



    이 프로그램 리스트에서는 4번줄에서 배열변수 a[]를 16비트형 정수로 선언해 주었다. 따라서 이 변수들에 각각 2바이트씩 메모리할당이 이루어진다. 이 프로그램을 실행시켜 보라.






    이와 같이 변수들은 주어진 데이터형에 따라, 메모리에서 차지하는 크기가 달라진다.



    2.5 프로그램의 저장영역


    하나의 프로그램이 주 메모리에 로딩될 때, 운영체제는 프로그램을 코드(code)영역, 데이터(data)영역, 스택(stack)영역 등 3개의 영역으로 나누어 주메모리에 로딩한다. 코드영역에는 프로그램 실행코드가 저장되며, 데이터 영역에는 전역 데이터(global data)들이 저장되고, 스택영역에는 지역데이터(local data)들이 저장된다. 32비트 운영체제인 윈도즈는 4K바이트 크기를 갖는 페이지들을 이용하여 이러한 영역들을 생성한다. 다음은 프로그램의 main()함수가 저장되는 영역을 알아 보기 위한 프로그램이다.



    리스트 8: 코드(code)영역

    1: #include

    2: void main()

    3: {

    4: printf("pointer of main()=%p\n",main);

    5: }



    위 리스트의 4번 줄에서 main은 main()함수의 포인터가 된다. 함수 포인터에 대해서는 나중에 좀 더 설명하기로 한다. 위 프로그램을 실행시켜 보라.


    다음은 전역변수(global variables) 및 지역변수(local variables)의 저장영역을 알아 보기 위한 프로그램이다.



    리스트 9: 전역변수 및 지역변수의 저장영역

    1: #include

    2: int a,b;

    3: void main()

    4: {

    5: int a,b;

    6: printf("adress of variables=%p %p\n",&a,&b);

    7: printf("adress of variables=%p %p\n",&::a,&::b);

    8: }



    리스트 9의 3번 줄에서 정수형 전역변수 a와 b를 선언하고, 5번 줄에서 정수형 지역변수 a와 b를 선언한다. 6번 줄에서, 지역변수의 포인터(pointer)값을, 7번 줄에서 전역변수의 포인터(pointer) 값을 화면에 출력한다. 여기서 "&"는 변수의 포인터를 계산하는 번지 연산자이다. 포인터에 대해서는 나중에 자세히 다루기로 한다. "::"는 전역변수를 지정하는 descripter이다. 지역변수와 전역변수 또한 나중에 다시 다루기로 한다. 이 프로그램을 실행시켜 보라.



    지역변수는 스택영역에 저장되므로, 변수가 선언될 때마다, 변수가 선언된 순서대로 스택영역의 높은 메모리 주소부터 저장된다. 그림 8의 결과를 보면, 0x12ff7c번지에 지역변수 a를 할당하고, 0x12ff78번지에 지역변수 b를 할당함을 알 수 있다. 즉 지역변수가 하나 선언될 할 때마다, 할당된 메모리 주소는 4씩 줄어드는 것을 알 수 있다. 반면에 전역변수는 데이터영역에 저장되고, 변수가 선언될 때마다, 변수가 선언된 순서대로 데이터영역의 낮은 메모리 주소부터 저장된다. 그림 8의 결과를 보면, 전역변수a는 0x4255e0번지에 할당되고, 전역변수b는 0x4255e4번지에 할당됨을 알 수 있다. 즉 전역변수가 하나 선언될 때마다, 할당된 메모리 주소가 4씩 증가함을 알 수 있다.



    2.6 요약


    이번 장에서는 컴퓨터 메모리와 변수와의 관계를 살펴 보았다. 메모리에 데이타가 저장된 방식과 변수가 선언될 때, 메모리 주소에 할당되는 영역등에 대해서도 알아 보았다. 이렇게 메모리에 대한 이해는 보다 폭넓고 전문적인 프로그램에서는 절대적으로 필요하며, 특히 하드웨어 자원이 제한적인 시스템에서의 프로그램에서 요구되어진다.

     


    - 복귀 주소 (Return Address)

     프로시저가 해당 기능을 끝마친 후에 이 프로시저를 호출한 바로 다음 코드의 내용이 실행되어질 수 있도록 해당 정보를 가지고 있음

     프로시저가 호출되기 바로 직전에 저장해 놓고 호출한 프로시저가 끝마치게 되면 이 내용을 사용하고, 해당 프로시저가 끝나게 되면 복귀 주소 내용 역시 필요 없어짐

     프로시저 안에서 또 다른 프로시저의 호출 또는 재귀 호출과 같은 형태의 흐름이 가능함 (복귀 주소로 돌아오기 전에 또 다른 복귀 주소가 중첩하여 여러 개 존재할 수 있음)

     => 이러한 복귀 주소의 특성을 고려하여 컴퓨터 시스템에서는 스택이라는 메모리 공간을 정의해놓고, 복귀 주소 정보를 저장하고 있다.

     

    - 스택 로컬 변수 저장

     프로시저 내에서 사용하는 로컬 변수는 그 프로시저가 호출되어 실행될 때까지만 유효함

     즉, 프로시저가 호출될 때에만 그 프로시저에서 사용할 로컬 변수의 메모리 공간이 필요하고, 프로시저가 복귀하게 되면 이 영역에서 사용한 로컬 변수들 역시 쓸모없는 데이터가 되어버리기 때문에, 복귀 주소처럼 스택 구조에 저장

     스택 구조에서 쓰레기 값 영역에 대해 다음 번 프로세서가 덮어쓰기 형태로 사용하기 때문에 효율적으로 스택 메모리를 사용할 수 있음

     

    - 스택 파라미터 전달

     프로그램에서 어떠한 데이터를 프로시저에게 전달하고자 할 때 전달하고자 하는 데이터들을 어디엔가 저장하여 놓거나, 데이터가 저장된 위치를 발견하여 프로시저 내에서 사용하는 단계, 혹은 프로시저가 복귀 되어질 때 전달받은 데이터를 다시 해제하는 방법이 있음

     프로시저로의 데이터 전달 역시 전달하고자 하는 프로시저 내에서만 사용하면 되기 때문에 스택 영역에 저장

     파라미터가 있는 프로시저의 경우 프로시저가 호출되기 전에 전달하고자 하는 파라미터들을 스택에 저장하여 놓은 후 프로시저로 제어 흐름을 바꿈

     프로시저 내에서 중간에 스택의 내용을 변경하는 코드가 있는 경우를 해결하기 위해 스택이 변경되는 경우까지 고려하여 그 이후의 코드에 대해 스택의 위치를 새로 계산하여 올바른 파라미터를 가리킬 수 있도록 하고 있으며, 스택 프레임이라는 것을 구성하여 해결하고 있음

     * 스택 프레임

       호출된 프로시저의 복귀 주소를 기준으로 고정된 변위 값을 통하여 파라미터를 다루는 것

       파라미터 참조 기준을 push나 pop 등에 의해 스택 포인터의 값이 변경될 수 있는 스택 포인터(ESP) 기준이 아니라, 명령어에 의하여 영향을 받지 않는 EBP 레지스터를 기준으로 하여 문제를 해결함

       스택 프레임 구성은 로컬 변수 참조에도 편리함을 제공

     ESP, EBP 어셈블리 예제)

    push ebp // 이전 스택의 베이스 주소를 저장

    mov ebp, esp // 현재 스택의 꼭대기를 새로운 스택의 베이스로 설정 (새로운 스택 시작)

    보통 EBP는 데이터 접근을 위한 기준 주소를 저장, ESP는 최상위 주소를 유지하는  용도로 사용

    - 파라미터 전달 방법 (Calling Conventions)

     콜링 컨벤션 : 스택을 이용하여 파라미터를 전달할 때 스택에 파라미터를 어떤 순서로 넣을 것인지, 전달된 파라미터를 어느 곳에서 해제할 것인가에 따른 여러가지 방법

     * __cdecl

       C 또는 C++ 프로그램에서 파라미터 전달 시 디폴트로 사용하는 방식으로, 파라미터 전달은 오른쪽에서 왼쪽 방향으로 이루어지며 프로시저를 호출한 쪽에서 파라미터에 대한 해제까지 책임짐

     * __stdcall

       Windows API의 프로시저에서 사용하는 콜링 컨벤션 방식으로, 오픈쪽에서 왼쪽 방향으로 스택에 저장하며 파라미터의 해제는 프로시저가 복귀 되기 전에 이루어짐

       함수의 독립성이 뛰어나다는 장점이 있음 (프로시저를 부르기 전에 스택에 파라미터를 쌓아놓고 그 프로시저를 부르기만 하면 그 함수가 리턴되어진 후 호출한 프로시저에 대해 신경쓰지 않아도 되며, 스택을 해제하는 코드가 호출한 프로시저 안에 있으므로 여러 곳에서 호출되더라도 스택을 해제하는 코드는 하나만 존재하므로 코드의 크기가 줄어듬)

     * __fastcall

       처음 두 개의 파라미터는 스택을 사용하지 않고 ecx와 edx 레지스터를 사용하며, 그 이상의 파라미터에 대해서만 오른쪽에서 왼쪽 방향으로 스택에 저장하며, 스택 제거는 __stdcall과 동일함

     

    - Windows에서의 스택 구조

     어떤 프로그램이 함수를 부르거나 로컬 변수를 저장하기 위해서는 스택 공간이 필요하며, 윈도우즈에서는 이러한 공간을 스레드가 생성되는 단계에서 함께 만들며, 스레드 생성시 디폴터로 1MB의 메모리를 예약하여 놓고 이 중 1개의 페이지인 4KB만을 실제 메모리와 매핑시켜 놓음으로써 사용 가능하게 하고 또 하나의 메모리는 가드 페이지라는 형태로 만들어 둠

     기본적인 1MB 메모리에서 상위 첫 페이지의 4KB 영역은 Committed 영역으로 프로그램에서 바로 스택 영역으로 사용되어질 수 있는 영역이며, 그 아래 한 페이지인 4KB 영역은 가드 페이지로 윈도우즈에 의해 페이지 보호가 이루어지고 있으며, 그 외의 영역은 단지 예약만 해놓은 상태로 실제적인 물리적 메모리가 할당되지 않은 상태임 (예약 해놓음으로써 이 프로세스 공간에 대한 메모리 할당을 스택 용도 외로 사용하지 못하게 함)

     

    - 함수에서의 레지스터 사용

     컴퓨터에서 수행되는 대부분의 연산은 레지스터를 이용해 이루어지는데, 레지스터의 개수는 제한적이기 때문에 함수 내에서의 또 다른 함수 호출 시 레지스터간의 충돌을 고려해야 함

     Windows의 컴파일러들은 이러한 문제를 피하기 위해 Caller-save/Callee-save 레지스터와 같은 규칙을 사용하여 함수들간의 레지스터 사용의 충돌을 피함

     * Caller-save 레지스터 (scratch 레지스터 혹은 volatile 레지스터)

       함수 내에서 어떠한 임시적 저장 없이 마음대로 쓸 수 있는 레지스터로, 호출한 이전 함수에 어떠한 영향도 미치지 않는 레지스터들로 정의

       ex) RAX, RCS, RDX, R8~R11, ST(0)~ST(7), XMM0~XMM5, High half of XMM6~XMM15

     * Callee-save 레지스터

       호출한 함수에 영향을 미칠 수 있기 때문에 해당 레지스터를 사용하기 전에 반드시 저장을 해놓고 사용해야 하는 레지스터로, 해당 함수가 끝났을 때 반드시 이전의 값으로 돌려주어 이 함수를 호출한 함수에 어떠한 영향을 미쳐선 안 되는 레지스터들로 정의

       ex) RBX, RSI, RDI, RBP, R12~R15, XMM6~XMM15

     

    - Name Mangling (Name Decoration)

     함수를 선언하거나 전역 변수 등을 선언하였을 때, 사용되어진 이름들은 실제 컴파일 단계에서 일정한 규칙을 가지고 그 이름을 변경하는데 이를 네임 맹글링 혹은 네임 데코레이션이라 부름

     링커(Linker)가 다른 범위에 있는 같은 이름의 함수와 변수들에 대하여 구별할 수 있도록 함으로써 해당 함수 전체의 모듈에서 동일한 방법으로 사용되어지는지 체크할 수 있기 때문에 컴파일러 입장에서 중요한 작업이며, Namespace 안에 있는 변수의 이름에 대해서도 그 Namespace와 합해진 형태의 네임 맹글링을 만들게 됨

     

     

    '시스템' 카테고리의 다른 글

    dummy 크기 정확히 파악하기 - 출처 http://beist.org  (0) 2016.05.01
    코어덤프  (0) 2016.05.01
    리눅스 gdb 명령어 (계속 추가)  (0) 2016.02.28
    공유메모리  (0) 2016.02.18
    시스템 $0x80  (0) 2016.02.11

    + Recent posts