C언어가 cpu에 작동하기까지

대상 : C를 공부했지만 pointer에 대해서 이해가 잘 되지않아 문제를 겪고있고, 컴퓨터가 작동하는 방식을 좀 더 이해하기 위한 글.

코드는 컴파일을 하면 실행파일이 되고 우리는 그 실행파일을 실행한다.

C언어의 컴파일 과정

image

사진은 리눅스 gcc에서 컴파일 과정

C언어의 컴파일 과정은 다음과 같다.

  1. 전처리를 거쳐서 매크로를 처리한다.
    • #include, #define 이 대표적인 매크로이다.
  2. 컴파일러를 통해서 어셈블리 언어로 변환한다.
  3. 어셈블러를 통해서 목적파일로 변환한다.
    • 리눅스나 맥에서 커맨드라인에서 컴파일 해본사람은 알겠지만 여기서 gcc 결과물로 나오는.o 파일이 여기서 얘기하는 목적파일이다
  4. 링커를 통해서 여러개의 목적파일을 하나의 실행파일로 변환한다.

윈도우 환경이라면 여기서 최종 파일로 .exe 확장자가 붙은 파일이 있을 것이다. 이것은 우리의 하드디스크나 ssd에 담겨져있는 단순한 파일이다. 이것을 컴퓨터가 어떻게 실행하는지 알아보자.

어셈블리어? 이진수?

image

image

C 언어를 어셈블리어로 나타낸 모습.

컴파일 중간에 생성되는 어셈블리어(.s 파일)는 CPU에게 내리는 명령어이다.

이는 실제로 기계어와 1:1대응되는데, cpu의 종류마다 실제 명령어가 달라지고 거기에 대응하는 기계어도 달라진다.

이 기계어들은 모두 이진수로 이어져있고, 컴파일 단계 거친후의 파일들은 모두 이진파일(binary file 위에서 언급한 목적파일, 실행파일도 포함)로 존재한다.

앞으로 사진에 보이는 명령어는 모두 이진수를 어셈블리어로 해석해 이루어져있는걸로 생각하면 된다.

프로그램 실행하기

image

컴파일 결과로 우리의 디스크에 잠들어있는 실행파일은 실행할때 OS가 우리가 컴퓨터를 살때 따지는 메모리(RAM)에 이 파일을 올린다. 이 올려주는 os의 역할을 loader라 한다. cpu는 이 메인메모리에 있는 명령어들을 읽으면서 차례대로 수행한다.

메모리와 주소

메모리에는 주소가 존재하고, 각 주소마다 1byte의 저장소가 있어 그곳에 값을 저장할 수 있다. 1byte의 저장소는 8비트로 이루어져 총 8개의 2진수 값을 저장 할 수 있다.

2진수 = 기계어 = 어셈블리어

혹시 이렇게 생긴 메모리 구조에 대해서 본적이 있는가

image

Linux OS hosted on a 32-bit x86

OS는 프로세스를 실행할때마다 프로세스마다 논리적으로 이런 구조를 가지도록 구성하여 메모리에 할당 한다.(구체적인 구조는 OS마다 차이가 있다.)

이런 구조는 모든 프로세스가 동일한 메모리 구조에서 작동하게 하기위해서 짜여진 논리적인 구조고 보통 프로세스 메모리 구조라고 부른다.

모든 프로세스가 동일한 메모리 주소 범위를 가지고 있는데, 가리키는 주소들은 이는 실제 메모리(물리메모리라 한다)를 가리키는 주소가 아니고 가상 메모리라한다 가상 메모리를 참조할때에 OS와 CPU에서는 실제 메모리 주소를 가리키는 변환과정을 거친다. 그래서 실제 개발자 입장에서 물리 메모리를 생각할 일은 잘 없다.

참고로 위의 구조에서 CPU에게 명령을 내리는 기계어들은 .text 구역에 있고. 지역변수 할당들은 .stack 영역에서 이루어진다. 실행 파일은 실행될때 요 구조 세팅을 담당하는 내용들이 기재되어있다.

image

프로세스 생성시에 생성되는 이 메모리 구조는 실제 물리메모리에는 쪼개져서 할당된다.(일부는 하드디스크에도 / 할당 자체가 안되었을 수도 있다.)

pointer

pointer 자료형은 가지고있는 값을 이 프로세스의 가상 메모리 주소값으로 해석한다. pointer 연산을 통해 이 주소를 따라 저장되어 있는 값을 읽을 수 있다.

image

자료형에 따라서 한번에 읽은 주솟값이 달라진다. 예를들어 int 경우 기본 자료형의 크기가 4바이트이므로 pointer 연산시에 4바이트 만큼의 크기를 읽는다.

또한 ++연산시에 pointer의 자료형의 크기만큼 주솟값이 더해진다.

위 사진의 경우 int형 pointer자료형이 0012FF7C주소가 저장되어 있고 이것을 pointer 연산으로 읽는다면 0012FF7C, 0012FF7D , 0012FF7E, 0012FF7F의 4바이트를 읽고 ++연산시에는 0012FF80을 가리키게 된다.

만약 char *형으로 한다면 ++시에는 0012FF7D 가 될거고 short *이라면 0012FF7E가 된다.

포인터 자료형 자체의 크기는 32bit일땐 4byte, 64bit 일땐 8byte이다.

만약에 자료형이 char형이라면 0012FF7C의 1바이트값만 읽고 ++ 연산시에 0012FF7D을 가리킨다.


정리

image

해당 사진은 프로세스가 메모리에 올라가 명령어가 저장되어있는 부분을 캡처하였다.

  • 좌측 : 주소(가상 메모리 주소)
  • 가운데 : 그 주소에 저장소에 저장되어있는 이진수를 16진수로 나타낸 값
  • 우측 : 메모리에 있는 이진수를 어셈블리어로 해석한 모습(해당 어셈블리어는 x86-64)

이제 cpu는 이 기계어를 한줄씩 차례대로 읽고 그에따라 작동한다.

하나의 16진수 값으로 4비트를 표현할 수 있고 16진수 두개의 값으론 8비트인 1byte를 나타낼 수 있다.

주소 계산을 해보면 첫번째 줄에 16진수 값 두개씩 두번 나타내고 다음 주솟값은 이전 주솟값에 크기가 2증가 해 있음을 알 수 있다.


아직 이정도 얘기를 하면서 다 이해하지 못한 부분도 있을것이고(처음 보는 용어가 있을 수도 있고) 새로 생긴 궁금증도 있을것이다 이런 빈 공간은 앞으로 시스템 프로그래밍, 운영체제, 컴퓨터 구조를 학습하며 채워나가자.

사진 출처 :

  • CS:APP
  • https://gabrieletolomei.wordpress.com/miscellanea/operating-systems/in-memory-layout/
  • https://mystyle1057.tistory.com/entry/C%EC%96%B8%EC%96%B4-%EA%B0%95%EC%A2%8C-%ED%8F%AC%EC%9D%B8%ED%84%B0%EB%B3%80%EC%88%98%EC%9D%98-%EA%B0%92%EA%B3%BC-%EC%A3%BC%EC%86%8C%ED%8F%AC%EC%9D%B8%ED%84%B0-%EA%B8%B0%EC%B4%88
  • http://blog.skby.net/%EA%B0%80%EC%83%81-%EB%A9%94%EB%AA%A8%EB%A6%AC-virtual-memory/
  • https://www.wbcsmadeeasy.in/loader-and-linker-computer-science-notes-for-w-b-c-s-examination/
Posted 2021-10-27