Notice
Recent Posts
Recent Comments
Link
«   2025/04   »
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 29 30
Tags
more
Archives
Today
Total
관리 메뉴

영우

소스코드에서 실행까지 본문

CS/운영체제

소스코드에서 실행까지

duddn 2024. 4. 19. 12:50

GPT 생성 이미지

들어가며

C나 C++같은 고급언어로 소스코드를 작성하고, 소스코드를 컴파일해 실행파일을 만들고, 실행파일을 실행해 프로그램을 사용해왔습니다. 이런 과정들이 복잡하게 생각하지 않아도 될 만큼 잘되어있어서 내부적으로 어떤 일을 하는것인지 잘 알지 못했습니다.

오늘은 미지의 세계인 소스코드부터 실행까지 어떤 일이 일어나는지 알아보았습니다.(요람에서 무덤까지를 패러디했습니다. ㅋㅋ..)

전체적인 절차

https://www.baeldung.com/cs/compiler-linker-assembler-loader

  • 고급언어로 작성한 소스코드로부터 시작합니다. 이를 컴파일러가 기계어에 보다 가까운 어셈블리어로 만들어 줍니다.
  • 여전히 어셈블리어는 기계어가 아니라서 실행시킬 수 없습니다. 그렇기 때문에 어셈블리 코드를 바탕으로 어셈블러가 이진파일인 목적파일로 만들어줍니다.
  • 그리고 링커가 목적파일들과 외부 라이브러리를 링킹해 하나의 실행파일로 만들어줍니다.
  • 마지막으로 프로그램을 실행하면 로더가 실행파일을 메모리에 로드합니다.

소스코드에서 실행까지 4개의 프로그램이 관여하는것을 확인할 수 있습니다. 이를 단계별로 확인해보겠습니다.

컴파일러

https://www.baeldung.com/cs/how-compilers-work

  • 컴파일러의 목적은 소스코드를 기계에 가까운 어셈블리 코드로 변경하는 것입니다. 추가적인 목적으로 잘못된 구문을 감지해 개발자에게 구문오류를 알릴 수 있습니다.
  • 컴파일 과정은 여러가지 단계로 나누어집니다.

어휘분석

  • 입력: x = y * 35
  • 출력:
    • Identifier(x)
    • Assignment Operator(=)
    • Identifier(y)
    • Multiplication Operator(*)
    • Integer Constant(35)

어휘분석 단계에서 소스코드를 일련의 토큰 시퀸스로 변경합니다. 각 토큰에 들어가는 정보로 소스코드의 데이터 뿐만 아니라 변수인지 연산자인지 같은 정보도 포함합니다.

구문분석

  • 입력: Identifier(x), Assignment Operator(=), Identifier(y), Multiplication Operator(``), Integer Constant(35)
  • 출력: 추상 구문 트리(AST)
    Assignment
    /       \
   x        Mul
           /   \
          y     35

구문분석 단계에서 토큰 시퀸스를 분석해 구문이 올바른지 확인합니다. 올바르지 않다면 개발자에게 알려주고, 올바르다면 이 토큰을 바탕으로 프로그램의 논리적 구조를 나타내는 추상구문트리(AST)를 생성합니다.

의미론적 분석

  • 입력: AST
  • 검사:
    • y가 정의되어 있고 사용 가능한가?
    • x의 타입이 결과값을 저장하기에 적합한가?
  • 출력: 주석이 달린 AST(타입 정보 포함)

의미론적 분석에서 AST를 검사해 의미적 오류를 찾습니다. AST를 완전히 순회하며 사용하는 변수가 정의 되었는지, 변수에 적절한 유형의 데이터가 할당되었는지 같은 검사를 합니다. 최종적으로는 타입정보를 포함해 주석이 달린 AST를 생성합니다.

중간코드 생성

  • 입력: 주석이 달린 AST
  • 출력:
    t1 = int_to_real(35)
    t2 = y * t1
    x = t2

중간코드는 컴파일러가 소스코드를 변환하는 과정에서 사용하는 중간 표현입니다. 이식성을 위해 하드웨어에 종속적이지 않은 형태이며, 최적화를 수행하기 쉽게 만듭니다.

그리고 소스코드와 어셈블리어의 중간정도의 추상화 레벨이기 때문에 소스코드 수준의 최적화와 하드웨어 수준의 최적화를 모두 수행할 수 있습니다.

최적화

t1 = y * 35   // 35는 컴파일 타임에 알려진 상수이므로 이 곱셈을 더 효율적인 연산으로 변환할 수 있음
x = t1

최적화 프로세스의 목표는 중간코드를 수정해 프로그램의 원래 의미를 변경하지 않는 선에서 더 적은 리소스를 소비하고 소프트웨어의 속도를 높이는것입니다.
함수 인라인, 데드코드 제거, 명령어 제거같은 기법을 활용해 최적화를 수행할 수 있습니다.

코드생성

; 어셈블리 언어로 구현된 전체 코드
; 가정: y, x는 레지스터 또는 메모리 주소에 저장된 변수

; y의 값을 레지스터 R1로 로드
MOV R1, y    ; R1 = y

; R1의 값에 35를 곱하여 R2에 저장 (최적화를 위해 시프트와 덧셈 사용)
MOV R2, R1   ; R2 = R1
SHL R2, 5    ; R2 = R2 * 32 (왼쪽으로 5비트 시프트, R1 * 32)
ADD R2, R1   ; R2 = R2 + R1 (R2에 R1을 더함)
ADD R2, R1   ; R2 = R2 + R1 (R2에 R1을 더함, 총 R2 = R1 * 35)

; 계산된 결과 R2를 x에 저장
MOV x, R2    ; x = R2

; 프로그램 완료

최적화된 중간코드를 어셈블리어로 변환합니다.

어셈블러

어셈블리어란?

어셈블리 언어가 등장하기 전 프로그래머는 작업을 하기 위해 0과 1로 이루어진 기계어로 프로그램을 개발하는 작업을했습니다. 당연히 프로그래머는 CPU의 명령어 구조와 CPU 명령어들의 숫자코드를 정확히 알아야했고 이는 사람이 이해하기 어렵고 실수하기 쉬운 작업이었습니다.

어셈블리어는 명령어와 대응되는 숫자코드 대신 자연어에 보다 가까운 명령세트를 가지고 있고 이를 사용해 코딩합니다. 여전히 기계어의 명령과 1대1 대응이 되기때문에 CPU에 종속된 언어입니다.

어셈블러란?

https://www.geeksforgeeks.org/introduction-of-assembler/

어셈블러는 어셈블리 코드를 기계어로 바꿔주는 프로그램입니다. 어셈블리어는 기계어와 1대1 대응이기 때문에 어셈블러는 명령어 테이블만 내장하고 각 구문을 테이블에 맞게 치환만 하면됩니다.

어셈블러가 출력한 기계어로 된 파일각각을 목적파일이라고 합니다. 소스코드의 파일 하나당 목적파일이 하나 생성됩니다.

링커

링커는 어셈블러가 생성한 목적파일을 결합해, 실행가능한 단일 이진파일인 실행파일을 생성하는 프로그랩입니다. 링커는 각 목적파일에 정의된 심볼(함수, 변수 등)과 외부 심볼에 대한 참조를 해석합니다. 외부 심볼일 경우 그 함수의 주소를 찾아 연결하는 작업을 하게 됩니다. 아직 실제 메모리에 로드가 안되었기 때문에 실제 메모리상의 주소는 알 수 없어 상대적인 주소로 참조시킵니다. 그리고 외부 라이브러리 같은 런타임에만 알 수 있는 주소는 재배치가능한 코드로 남겨두어 로더가 실제 메모리 주소를 알게 되었을때 주소를 재배치하도록 합니다.

정적링킹

https://www.linkedin.com/pulse/difference-between-static-dynamic-linking-amr-abdel-fattah

정적링킹에서 링커는 모든 의존하는 코드를 최종적인 실행파일에 복사합니다. 외부 라이브러리를 연결할때도 해당 라이브러리가 필요로하는 의존성을 찾아 실행파일에 복사하게 됩니다. 그렇기때문에 정적링킹에서 프로그램의 코드에 모든 의존하는 라이브러리의 코드가 포함되게 됩니다. 그래서 실행파일이 매우 커지고 디스크상에서나 메모리상에서나 공간이 많이 필요합니다.

장점으로는 한 프로세스가 다른 프로세스가 메모리를 공유하지 않으므로 완전히 격리되고, 컴파일타임에 모든 의존성이 해결되므로 런타임에 더 빠르다는 장점이 있습니다.

그러므로 보안이 극도로 중요해 모든 프로세스를 격리해야되는 환경이나 임베디드 같은 런타임 실행이 중요한 환경에서 사용할 가치가 있습니다.

동적링킹

https://www.linkedin.com/pulse/difference-between-static-dynamic-linking-amr-abdel-fattah

동적링킹에서 링커는 외부 라이브러리의 의존성을 해결하지 않고 실행파일을 만듭니다. 운영체제는 실행 중 해결되지 않은 라이브러리를 사용하면 RAM에 쿼리해 해당 라이브러리가 로드되었는지 확인합니다. 라이브러리가 메모리에 상주하고 있다면 사용하고, 그렇지 않다면 라이브러리를 메모리에 로드합니다. 결과적으로 여러 프로세스가 하나의 라이브러리를 사용하더라도 메모리상에서는 하나의 라이브러리만 존재할 수 있습니다.

장점으로는 메모리를 효율적으로 사용할 수 있습니다. 프로그램의 실행파일에 외부 라이브러리가 들어가지 않으므로 크기가 더 작고, RAM에 중복된 라이브러리가 로드되지 않습니다. 그러므로 캐시의 장점도 동시에 얻을 수 있습니다.

공통 라이브러리 세트를 사용하는 애플리케이션이 많을때 동적 연결을 사용할 수 있습니다.

로더

로더는 실행파일을 바탕으로 메인 메모리에 로드하고 컴퓨터에 실행 될 수 있도록 합니다. 메모리를 할당 받아 프로그램의 코드를 로딩하고, 실제 메모리 위치가 나왔기 때문에 메모리의 위치를 반영하도록 프로그램의 메모리 주소를 재배치합니다. 모든 준비가 완료되면 로더는 CPU에 프로그램의 시작 지점을 알려 프로세스의 실행을 시작합니다.

출처

'CS > 운영체제' 카테고리의 다른 글

프로세스간 통신  (0) 2024.05.03