처음 프로그래밍을 시작했을 때 우리가 완료한 첫 번째 작은 프로젝트는 "hello world"였습니다. 짧은 시간에 우리는 이 언어로 hello world를 작성할 수 있었습니다. 그러나 보지 마십시오. 단지 몇 글자에 지나지 않습니다. 그러나 대부분의 사람들은 여전히 간단한 프로그램 hello world의 내부 작동 메커니즘을 설명할 수 없으므로 오늘은 프로그램의 작동 메커니즘에 대해 설명하겠습니다.
hello world 이 메시지는 모니터에 어떻게 표시되나요? CPU에 의해 실행되는 코드는 우리가 프로그램에서 작성하는 코드와 확실히 다릅니다. 우리가 작성한 코드에서 CPU가 실행할 수 있는 코드로 어떻게 바뀌나요? 프로그램이 실행될 때 코드는 어디에 있습니까? 어떻게 구성되어 있나요? 프로그램의 변수는 어디에 저장되나요? 함수 호출은 어떻게 나타나나요? 이 기사에서는 프로그램이 어떻게 작동하는지 간략하게 설명합니다.
개발 플랫폼의 숨겨진 프로세스
모든 언어에는 자체 개발 플랫폼이 있으며 대부분의 프로그램이 여기서 탄생합니다. 프로그램 소스 코드에서 실행 파일로 변환하는 과정은 실제로 여러 단계로 나누어져 매우 복잡합니다. 그러나 현재 개발 플랫폼은 이러한 모든 작업을 자체적으로 수행하므로 우리에게 편리함을 가져다 줄 뿐만 아니라 많은 구현이 숨겨져 있습니다. 세부. 따라서 대부분의 프로그래머는 코드 작성만 담당하고 기타 복잡한 변환 작업은 개발 플랫폼에서 자동으로 완료됩니다.
제가 이해한 바에 따르면, 간단히 말하면 소스 코드에서 실행 파일까지의 과정은 다음과 같은 단계로 나눌 수 있습니다.
1. 소스 코드에서 기계어까지 그리고 생성된 기계어를 일정한 규칙에 따라 정리합니다. 지금은 파일 A라고 부르겠습니다.
2. A 파일을 실행하여 A+
3 파일을 메모리에 로드하고 파일을 실행하는 데 필요한 파일 B(라이브러리 기능 등)와 A 파일을 연결합니다. 기타 정보 이 외에도 더 많은 단계가 있을 수 있지만 단순화하기 위해 여기서는 3단계로 요약합니다.)
실행 파일을 구성하는 핵심 단계이며, 하나도 빠질 수 없습니다. 이제 당신은 개발 플랫폼에 의해 "눈이 멀었다"는 것을 알 수 있습니다. 다음 섹션에서는 안개를 걷어내고 개발 플랫폼의 진정한 모습을 보여줍니다.
객체 파일
컴퓨터 분야의 고전적인 격언이 있습니다.
"컴퓨터 과학의 모든 문제는 또 다른 간접 계층으로 해결될 수 있습니다."
"컴퓨터 과학의 모든 문제는 중간 계층을 추가하면 해결될 수 있습니다.”
예를 들어 A를 B로 변환하려면 먼저 A를 파일 A+로 변환한 다음 파일 A+를 필요한 파일 B로 변환하면 됩니다. (실제로 이 방법은 폴리아의 "How to Love it"에도 설명되어 있습니다. 문제를 해결할 때 중간 레이어를 추가하면 문제를 단순화할 수 있습니다.)
그러면 소스 코드에서 실행 파일까지의 과정을 이렇게 이해할 수 있습니다. 문제를 해결하기 위해 소스 코드에서 실행 파일 사이에 (지속적으로) 중간 레이어를 추가하여 실행 파일로 이동하는 경우에도 마찬가지입니다.
위에서 언급했듯이 먼저 소스 프로그램을 중간 파일 A로 변환한 다음 중간 파일을 필요한 대상 파일로 변환합니다.
파일을 처리할 때 가는 방법입니다.
사실 위에서 언급한 파일 A에 대한 보다 전문적인 용어는 대상 파일입니다. 실행 가능한 프로그램이 아니므로 실행되기 전에 다른 대상 파일과 연결하고 로드해야 합니다. 소스 프로그램의 경우 개발 플랫폼이 가장 먼저 해야 할 일은 소스 프로그램을 기계어로 번역하는 것입니다. 그것의 매우 중요한 부분은 컴파일입니다. 소스 코드를 기계어(실제로는 바이너리 코드 묶음)로 번역하는 것이라는 사실을 많은 분들이 알고 계실 거라 믿습니다. 편집 지식은 매우 중요하지만 이 기사의 초점은 아닙니다. 관심이 있으시면 직접 구글링해 보시기 바랍니다.
대상 파일 형식:
이제 위에서 언급한 대상 파일이 어떻게 구성되어 있는지(즉, 저장 구조)를 살펴보겠습니다.
원산지:
당신이 이 바이너리 코드를 디자인했다면 어떻게 구성할지 상상해 보세요. 책상 위의 물건을 분류하여 깔끔하게 정리해야 하듯이, 번역된 바이너리 코드도 관리의 용이성을 위해 카테고리별로 저장하여 코드를 나타내는 것과 데이터를 나타내는 것을 함께 보관합니다. 이러한 방식으로 바이너리 코드는 저장을 위해 여러 블록으로 나누어집니다. 그러한 영역을 세그먼트(Segment)라고 합니다.
표준:
컴퓨터 과학의 많은 것들처럼 사람들의 의사소통, 프로그램 호환성 및 기타 문제를 용이하게 하기 위해. 이 바이너리 저장 방식에 대한 표준도 개발되어 COFF(Common Object File Format)가 탄생했습니다. Windows, Linux 등 현재 주류 운영 체제에서 대상 파일 형식은 COFF와 유사하며 COFF의 변형으로 간주될 수 있습니다.
a.out:
a.out은 대상 파일의 기본 이름입니다. 즉, 파일을 컴파일할 때 컴파일된 대상 파일의 이름을 바꾸지 않으면 컴파일 후에 a.out이라는 파일이 생성됩니다.
여기서 이 이름이 사용된 구체적인 이유는 다루지 않겠습니다. 관심이 있으시면 직접 구글링해 보시기 바랍니다.
아래 그림을 보면 대상 파일을 보다 직관적으로 이해할 수 있습니다.
위 그림은 실제 상황과 다를 수 있지만 모두 파생된 내용입니다. 이 기초.
ELF 파일 헤더: 위 그림의 첫 번째 세그먼트. 헤더는 대상 파일에 대한 일부 기본 정보를 포함하는 대상 파일의 헤더입니다. 파일 버전, 대상 기계 모델, 프로그램 항목 주소 등
텍스트 세그먼트: 내부 데이터는 주로 프로그램의 코드 부분입니다.
데이터 세그먼트: 변수와 같은 프로그램의 데이터 부분입니다.
재배치 세그먼트:
재배치 세그먼트에는 재배치 정보가 포함된 텍스트 재배치 및 데이터 재배치가 포함됩니다. 일반적으로 코드에는 외부 함수나 변수에 대한 참조가 있습니다. 참조이므로 이러한 함수와 변수는 대상 파일에 존재하지 않습니다. 이를 사용할 때 실제 주소가 제공되어야 합니다(이 프로세스는 링크 중에 발생합니다). 이러한 실제 주소를 찾기 위한 정보를 제공하는 것은 이러한 재배치 테이블입니다. 위 내용을 이해하고 나면 텍스트 재배치, 데이터 재배치가 이해하기 어렵지 않습니다.
기호 테이블: 기호 테이블에는 소스 코드의 모든 기호 정보가 포함되어 있습니다. 모든 변수 이름, 함수 이름 등을 포함합니다. 예를 들어 코드에 "student"라는 기호가 있으면 이 기호에 해당하는 정보가 기호 테이블에 포함됩니다. 이 기호가 위치한 세그먼트, 해당 속성(읽기 및 쓰기 권한) 및 기타 관련 정보를 포함합니다.
실제로 심볼 테이블의 원본 소스는 컴파일의 어휘 분석 단계에 있다고 할 수 있습니다. 어휘 분석을 수행할 때 코드의 각 기호와 해당 속성이 기호 테이블에 기록됩니다.
문자열 테이블: 기호 테이블과 유사한 기능을 가지며 일부 문자열 정보를 저장합니다.
한 가지 더 말씀드릴 점은 대상 파일이 모두 바이너리 파일, 즉 바이너리 파일로 저장된다는 것입니다.
실제로 대상 파일은 이 모델보다 더 복잡하지만 아이디어는 동일하며 유형에 따라 저장하고 대상 파일 정보와 링크에 필요한 정보를 설명하는 일부 섹션을 추가합니다.
a.out 분할
Hello World
은 빈 대화입니다. 이제 여기 C에서 설명하는 hello world가 컴파일된 후 형성된 개체 파일을 살펴보겠습니다.
간단한 hello world 소스 코드:
데이터 세그먼트에 넣을 데이터를 갖기 위해 여기에 "int a=5"를 추가합니다.
VC를 사용 중이라면 실행을 클릭하여 결과를 확인하세요.
내부적으로 어떻게 처리되는지 명확하게 확인하기 위해 GCC를 사용하여 컴파일합니다.
실행
gcc hello.c
디렉토리를 살펴보면 추가 대상 파일 a.out이 있습니다.
이제 우리가 하고 싶은 일은 a.out에 무엇이 있는지 확인하는 것입니다. 당시에는 vim 텍스트를 사용하여 보았던 것을 기억하는 아이들도 있을 것입니다. 그런데 a.out이 어떤 것인지, 어떻게 그렇게 쉽게 노출될 수 있는지. 예, vim이 작동하지 않습니다. "우리가 직면한 대부분의 문제는 이전 버전에서 발생하고 해결되었습니다." 예, objdump라는 매우 강력한 도구가 있습니다. 이를 통해 우리는 대상 파일의 다양한 세부 사항을 철저히 이해할 수 있습니다. 물론 나중에 소개할 매우 유용한 readelf도 있습니다.
이 두 도구는 일반적으로 Linux와 함께 제공되므로 직접 Google에서 검색할 수 있습니다.
참고: 여기에 있는 코드는 주로 Linux에서 GCC로 컴파일되며 Objdump 및 readelf는 대상 파일을 보는 데 사용됩니다. 하지만 실행 결과를 모두 사진에 담을 것이므로 이전에 Linux를 접해 본 적이 없는 분이라면 다음 내용을 읽어도 문제가 없을 것입니다. 우분투를 사용하고 있는데 느낌이 좋네요~
다음은 a.out의 조직 구조입니다: (각 세그먼트의 시작 주소, 크기 등)
대상 파일을 보는 명령은 objdump -h a.out입니다.
위에서 설명한 대상 파일과 형식이 동일하게 카테고리별로 저장되어 있는 것을 볼 수 있습니다. 대상 파일은 6개의 섹션으로 나뉩니다.
왼쪽에서 오른쪽으로 첫 번째 열(Idx Name)은 세그먼트 이름, 두 번째 열(Size)은 크기, VMA는 가상 주소, LMA는 물리적 주소, File off는 세그먼트 내의 오프셋입니다. 파일. 즉, 단락의 참조(보통 단락의 시작 부분)에 대한 이 단락의 거리입니다. 마지막 Algn은 세그먼트 속성에 대한 설명입니다. 지금은 무시하세요
"텍스트" 세그먼트: 코드 세그먼트.
"데이터" 세그먼트: 위에서 언급한 데이터 세그먼트로, 일반적으로 초기화된 데이터인 소스 코드에 데이터를 저장합니다.
"bss" 세그먼트: 초기화되지 않은 데이터를 저장하는 데이터 세그먼트이기도 합니다. 이러한 데이터는 아직 공간이 할당되지 않았기 때문에 별도로 저장됩니다.
"rodata" 세그먼트: 읽기 전용 데이터 세그먼트로, 여기에 저장된 데이터는 읽기 전용입니다.
"cmment"는 컴파일러 버전 정보를 저장합니다.
나머지 두 문단은 우리 논의에 실질적인 의미가 없으므로 다시 소개하지 않겠습니다. 링크, 컴파일 및 설치 정보가 포함되어 있다고 생각하십시오.
참고:
여기서 대상 파일 형식은 실제 상황의 주요 부분만 나열합니다. 표에 나열되지 않은 실제 상황도 있습니다. Linux도 사용하는 경우 objdump -X를 사용하여 더 자세한 세그먼트 내용을 나열할 수 있습니다.
Deep into a.out
위 부분에서는 예제를 통해 대상 파일의 일반적인 세그먼트를 설명하며, 주로 크기 및 기타 관련 속성과 같은 세그먼트 정보를 설명합니다.
이 세그먼트에는 정확히 무엇이 저장되어 있나요? objdump를 사용해 보겠습니다.
objdump -s a.out -s 옵션을 통해 대상 파일의 16진수 형식을 볼 수 있습니다.
다음과 같이 결과를 봅니다.
위 그림과 같이 각 세그먼트의 16진수 표현이 나열됩니다. 그림이 두 개의 열로 나뉘어져 있음을 알 수 있습니다. 왼쪽 열은 16진수 표현이고 오른쪽 열은 해당 정보를 표시합니다.
더 확실한 것은 "rodata" 읽기 전용 데이터 세그먼트의 "hello world"입니다. . 아, 프로그램에 "hello"가 잘못 입력되고, 끝에 "w"가 추가된 것 같습니다. 용서해주세요.
"hello world"의 ASCII 값도 확인할 수 있으며, 해당 16진수 값은 내부 내용입니다.
위에 언급된 "주석" 단락에는 일부 컴파일러 버전 정보가 포함되어 있습니다. 이 단락 뒤의 내용은 GCC 컴파일러와 버전 번호입니다.
a.out disassemble
컴파일 과정에서는 항상 소스 텍스트를 먼저 어셈블리 형식으로 변환한 다음 기계어로 번역합니다. (중간 레이어 추가) a.out을 너무 많이 본 후에는 해당 어셈블리 형식을 연구해야 합니다. Objdump -d a.out은 파일의 어셈블리 형식을 나열할 수 있습니다. 그러나 여기에는 주요 부분, 즉 주요 기능 부분만 나열되어 있습니다. 사실, 주요 기능의 실행 초기와 실행 후에는 아직 해야 할 일이 많이 남아 있습니다.
즉, 함수 실행 환경을 초기화하고 함수가 차지하는 공간을 해제하는 등의 작업을 수행합니다.
위 그림에서 왼쪽은 16진수 형태의 코드이고, 왼쪽은 어셈블리 형태입니다. 어셈블리에 익숙한 아이들은 대부분 이해할 수 있을 것이므로 여기서는 자세히 설명하지 않겠습니다.
a.out 헤더 파일
대상 파일 형식을 소개할 때 대상 파일의 기본 정보를 포함하는 헤더 파일 개념이 언급되었습니다. 파일 버전, 대상 기계 모델, 프로그램 항목 주소 등
아래 그림은 파일 헤더 형식입니다.
readelf -h를 사용하여 볼 수 있습니다. (아래 사진에 보이는 것은 hello.o인데, hello.c 소스파일에 의해 링크되지 않고 컴파일된 파일이다. 이는 a.out을 보는 것과 거의 동일하다)
사진 두 개의 Column으로 나누어지며, 왼쪽 열은 속성을 나타내고, 오른쪽 열은 속성값을 나타냅니다. 첫 번째 행은 종종 매직 넘버(magic number)라고 불립니다. 다음은 일련의 숫자입니다. 구체적인 의미에 대해서는 자세히 설명하지 않겠습니다.
다음은 대상 파일과 관련된 몇 가지 정보입니다. 우리가 논의하고 싶은 문제와는 밀접한 관련이 없으므로 여기서는 논의하지 않겠습니다.
위 내용은 특정 예를 사용하여 대상 파일의 내부 구성 형태를 설명합니다. 대상 파일은 실행 파일을 생성하는 과정의 중간 프로세스일 뿐이며 대상 파일이 어떻게 실행되는지에 대한 논의는 없습니다. 실행 파일로 변환되며 실행 파일이 실행되는 방법은 다음 섹션에서 설명합니다
링크에 대한 간단한 이해
일반인의 용어로 링크는 여러 개의 실행 파일입니다.
프로그램 A가 파일 B에 정의된 함수를 참조하는 경우 A의 함수가 정상적으로 실행되기 위해서는 B의 함수 부분을 A의 소스 코드에 배치한 다음 A와 B를 하나의 파일로 병합해야 합니다. 연결하고 있습니다.
프로그램을 연결하는 데에는 링커라고 하는 특별한 프로세스가 있습니다. 그는 일부 입력 대상 파일을 처리하고 이를 출력 파일로 합성합니다. 이러한 대상 파일에는 상호 데이터 및 함수 참조가 있는 경우가 많습니다.
위에서 hello world의 디스어셈블리 형태를 보았습니다. 연결되지 않은 파일이므로 외부 함수를 참조할 때 해당 주소를 알 수 없습니다.
아래와 같이:
위 그림에서 cal 명령어는 printf() 함수를 호출합니다. 현재 파일에 printf() 함수가 없기 때문에 해당 주소를 16진수로 표시하려면 "ff ff ff"를 사용하세요. 주소. 링크 후에 함수가 파일에 로드되었기 때문에 이 주소는 함수의 실제 주소가 됩니다.
링크 분류: 링크는 A 관련 데이터나 기능이 하나의 파일로 병합되는 순서에 따라 정적 링크와 동적 링크로 나눌 수 있습니다.
정적 링크:
프로그램이 실행되기 전에 연결 작업을 완료하세요. 즉, 링크가 완료될 때까지 파일을 실행할 수 없습니다. 그러나 이는 라이브러리 기능과 같은 명백한 단점이 있습니다. 파일 A와 파일 B 모두 특정 라이브러리 기능을 사용해야 하는 경우 링크가 완료된 후 링크된 파일은 이 라이브러리 기능을 갖게 됩니다. A와 B가 동시에 실행되면 메모리에 라이브러리 함수의 복사본이 두 개 있게 되는데, 이는 의심할 여지 없이 저장 공간을 낭비합니다. 이러한 낭비는 규모가 증가할 때 특히 두드러집니다. 정적 링크는 업그레이드가 어렵다는 단점도 있습니다. 이러한 문제를 해결하기 위해 오늘날 많은 프로그램에서는 동적 연결을 사용합니다.
동적 연결: 정적 연결과 달리 동적 연결은 프로그램이 실행될 때 수행됩니다. 이때 프로그램이 로드되고 실행됩니다. 위의 예에서 A와 B가 모두 라이브러리 함수 Fun()을 사용하는 경우 A와 B가 실행될 때 Fun()의 복사본 하나만 메모리에 있어야 합니다.
링크에 대한 지식은 아직 많이 있으며, 이에 대해서는 향후 관련 기사에서 논의할 예정입니다. 여기서는 자세히 다루지 않겠습니다.
로딩에 대한 간단한 설명
우리는 프로그램이 실행되기 위해서는 메모리에 로드되어야 한다는 것을 알고 있습니다. 과거 머신에서는 전체 프로그램이 물리적 메모리에 로드되는 것이 일반적이었습니다. 즉, 각 프로세스는 완전한 주소 공간을 갖고 있어 각 프로세스가 메모리를 사용할 수 있다는 인상을 줍니다. 그런 다음 메모리 관리자는 가상 주소를 실제 물리적 메모리 주소에 매핑합니다.
위 설명에 따르면 프로그램의 주소는 가상 주소와 실제 주소로 구분할 수 있습니다. 가상 주소는 가상 메모리 공간에 있는 주소이고, 실제 주소는 실제로 로드되는 주소입니다.
아마도 위의 세그먼트를 볼 때 파일이 연결되거나 로드되지 않았기 때문에 각 세그먼트의 가상 주소와 물리적 주소가 0이라는 것을 눈치챘을 것입니다.
로드 프로세스는 다음과 같이 이해할 수 있습니다. 먼저 프로그램의 각 부분에 가상 주소를 할당한 다음 가상 주소에서 실제 주소로의 매핑을 설정합니다. 실제로 중요한 부분은 가상 주소에서 물리적 주소로의 매핑 과정입니다. 프로그램이 설치된 후, CPU의 프로그램 카운터 PC는 파일 내 코드의 시작 위치를 가리키며, 프로그램이 순차적으로 실행됩니다.
이 글을 쓰는 목적은 프로그램 작동 메커니즘과 실행 파일 실행 뒤에 무엇이 숨겨져 있는지 정리하는 것입니다. 소스 코드부터 실행 파일까지 일반적으로 많은 중간 단계가 있으며 각 단계는 중간 파일을 생성합니다. 단지 현재의 통합 개발 환경에서는 이러한 단계가 숨겨져 있을 뿐입니다. 통합 개발 환경에 익숙한 우리는 점차 이러한 중요한 기술 내부자를 무시해 왔습니다. 이 문서에서는 이 프로세스의 주요 라인만 소개합니다. 각 세부 사항은 기사에서 논의될 수 있습니다.
이 기사를 읽고 나면 모든 사람들이 "hello world"가 단순한 작은 실험이라고 생각하지 않을 것이라고 생각합니다. 또한 이 기사를 통해 모든 사람들이 프로그램의 작동 메커니즘이 무엇인지, 어떻게 작동하는지 이해하기를 바랍니다.
관련 권장사항:
위 내용은 Hello World의 프로그램 운영 메커니즘에 대해 이야기하기의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!