source

각 OS에서 C/C++를 재컴파일해야 하는 이유는 무엇입니까?

factcode 2022. 8. 11. 21:56
반응형

각 OS에서 C/C++를 재컴파일해야 하는 이유는 무엇입니까?

이것은 다른 어떤 것보다도 이론적인 질문이다.저는 낮은 수준의 프로그래밍에 관심이 많은 Comp sci 전공자입니다.나는 비밀리에 일이 어떻게 돌아가는지 알아보는 것을 좋아한다.저의 전문은 컴파일러 디자인입니다.

어쨌든, 첫 컴파일러 작업을 하고 있을 때, 뭔가 혼란스러운 일이 일어나고 있습니다.

C/C++로 프로그램을 작성할 때, 전통적으로 사람들이 알고 있는 것은 C/C++ 코드가 마법처럼 그 기계의 네이티브 코드로 변환된다는 것입니다.

하지만 뭔가 앞뒤가 맞지 않아요.x86 아키텍처를 대상으로 C/C++ 프로그램을 컴파일하면 동일한 아키텍처의 컴퓨터에서 동일한 프로그램이 실행되어야 합니다.하지만 그런 일은 일어나지 않아요.OS X, 리눅스 또는 윈도우즈용 코드를 다시 컴파일해야 합니다.(32비트와 64비트의 경우에도 마찬가지)

왜 이런지 궁금해서요C/C++ 프로그램을 컴파일할 때 CPU 아키텍처/명령어 세트를 대상으로 하지 않습니까?또한 Mac OS와 Windows OS는 거의 동일한 아키텍처에서 실행될 수 있습니다.

(VM 또는 CLR의 Java 및 유사한 타깃을 알고 있으므로 해당 타깃은 포함되지 않습니다.)

여기서 베스트 샷의 답변을 얻으면 C/C++는 OS 고유의 명령으로 컴파일해야 합니다.하지만 내가 읽은 모든 소스는 컴파일러가 기계를 대상으로 한다고 말한다.그래서 나는 매우 혼란스럽다.

C/C++ 프로그램을 컴파일할 때 CPU 아키텍처/명령어 세트를 대상으로 하지 않습니까?

아니에요, 당신은 그렇지 않아요.

즉, CPU 명령 세트를 컴파일하고 있습니다.하지만 편찬이 다가 아닙니다.

가장 간단한 "안녕하세요, 세상!" 프로그램을 생각해 보십시오.전화만 하면 돼printf,그렇죠?그러나 printf 명령 집합 운영 코드는 없습니다.그럼 정확히 어떻게 되는 거죠?

음, 그건 C 표준 도서관의 일부야.그것의.printf함수는 문자열과 파라미터에 대해 몇 가지 처리를 수행합니다.그러면...이 표시됩니다.어떻게 그럴 수 있죠?음, 문자열을 표준 출력으로 보냅니다.좋아... 누가 조종하지?

운영 체제또, 「standard out」opcode도 없기 때문에, standard out 에 문자열을 송신하는 경우는, OS 의 콜이 몇개인가 필요합니다.

OS 콜은 운영체제 전체에서 표준화되지 않습니다.C 또는 C++에서는 독자적으로 구축할 수 없었던 표준 라이브러리의 거의 모든 기능은 OS와 대화하여 적어도 일부 작업을 수행합니다.

malloc메모리는 사용자의 것이 아니라 OS의 것이므로 메모리를 사용할 수 있습니다. scanf표준 입력은 사용자의 것이 아닙니다.OS에 속해 있기 때문에 읽을 수 있습니다.기타 등등.

표준 라이브러리는 콜에서 OS 루틴까지 구축되어 있습니다.이러한 OS 루틴은 포터블이 아니기 때문에 표준 라이브러리 구현은 포터블이 아닙니다.따라서 실행 파일에는 이러한 포터블 이외의 콜이 포함되어 있습니다.

게다가 OS 마다 「실행 가능 파일」이 어떻게 보이는지에 대한 생각이 다릅니다.실행 파일은 단순히 opcode의 집합이 아닙니다.이러한 일관성과 사전 초기화가 어디에서 이루어졌다고 생각하십니까?static변수가 저장됩니까?OS마다 실행 파일을 시작하는 방법이 다르며 실행 파일의 구조는 그 일부입니다.

메모리는 어떻게 할당합니까?동적 메모리를 할당하기 위한 CPU 명령은 없습니다.메모리는 OS에 문의해야 합니다.하지만 어떤 매개 변수들이 있을까요?OS를 어떻게 기동합니까?

출력은 어떻게 인쇄합니까?파일을 열려면 어떻게 해야 하나요?타이머는 어떻게 맞춰요?UI를 표시하려면 어떻게 해야 합니까?이 모든 것은 OS에서 서비스를 요청해야 하며 OS마다 필요한 콜이 다른 서비스를 제공합니다.

x86 아키텍처를 대상으로 C/C++ 프로그램을 컴파일하면 동일한 아키텍처의 컴퓨터에서 동일한 프로그램이 실행되어야 합니다.

사실이지만 몇 가지 뉘앙스가 있습니다.

C언어의 관점에서 OS에 의존하지 않는 프로그램의 몇 가지 사례를 생각해 보겠습니다.


  1. 프로그램이 처음부터 수행하는 모든 작업은 I/O 없이 많은 계산을 수행하여 CPU를 스트레스 테스트하는 것이라고 가정합니다.

머신 코드는 모든 OS에서 완전히 같을 수 있습니다(모든 OS가 동일한 CPU 모드(x86 32비트 보호 모드 등).어셈블리 언어로 직접 작성할 수도 있습니다.각 OS에 맞게 조정할 필요는 없습니다.

그러나 각 OS는 이 코드를 포함하는 바이너리에 대해 서로 다른 헤더를 원합니다.예를 들어 Windows는 PE 포맷, Linux는 ELF, macOS는 Mach-O 포맷을 사용합니다.심플한 프로그램에서는 머신 코드를 별도의 파일로 준비하고 각 OS의 실행 가능한 포맷에 대한 헤더 묶음을 준비할 수 있습니다.그러면 실제로 헤더와 머신 코드를 연결하여 얼라인먼트 "footer"를 추가하는 것만으로 '재컴파일'할 수 있습니다.

C 코드를 기계 코드로 컴파일했다고 가정합니다.이러한 코드는 다음과 같습니다.

offset:  instruction  disassembly
    00:  f7 e0        mul eax
    02:  eb fc        jmp short 00

이것은 간단한 스트레스 테스트 코드입니다. 반복하여 반복하여eax스스로 등록하다

이제 32비트 Linux 및 32비트 윈도우즈에서 실행하려고 합니다.2개의 헤더가 필요합니다(16진수 덤프).

  • Linux의 경우:
000000 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00  >.ELF............<
000010 02 00 03 00 01 00 00 00 54 80 04 08 34 00 00 00  >........T...4...<
000020 00 00 00 00 00 00 00 00 34 00 20 00 01 00 28 00  >........4. ...(.<
000030 00 00 00 00 01 00 00 00 54 00 00 00 54 80 04 08  >........T...T...<
000040 54 80 04 08 04 00 00 00 04 00 00 00 05 00 00 00  >T...............<
000050 00 10 00 00                                      >....<
  • Windows 의 경우(*아래 주소까지 앞줄을 반복하기만 하면 됩니다.*도달) :
000000 4d 5a 80 00 01 00 00 00 04 00 10 00 ff ff 00 00  >MZ..............<
000010 40 01 00 00 00 00 00 00 40 00 00 00 00 00 00 00  >@.......@.......<
000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  >................<
000030 00 00 00 00 00 00 00 00 00 00 00 00 80 00 00 00  >................<
000040 0e 1f ba 0e 00 b4 09 cd 21 b8 01 4c cd 21 54 68  >........!..L.!Th<
000050 69 73 20 70 72 6f 67 72 61 6d 20 63 61 6e 6e 6f  >is program canno<
000060 74 20 62 65 20 72 75 6e 20 69 6e 20 44 4f 53 20  >t be run in DOS <
000070 6d 6f 64 65 2e 0d 0a 24 00 00 00 00 00 00 00 00  >mode...$........<
000080 50 45 00 00 4c 01 01 00 ee 71 b4 5e 00 00 00 00  >PE..L....q.^....<
000090 00 00 00 00 e0 00 0f 01 0b 01 01 47 00 02 00 00  >...........G....<
0000a0 00 02 00 00 00 00 00 00 00 10 00 00 00 10 00 00  >................<
0000b0 00 10 00 00 00 00 40 00 00 10 00 00 00 02 00 00  >......@.........<
0000c0 01 00 00 00 00 00 00 00 03 00 0a 00 00 00 00 00  >................<
0000d0 00 20 00 00 00 02 00 00 40 fb 00 00 03 00 00 00  >. ......@.......<
0000e0 00 10 00 00 00 10 00 00 00 00 01 00 00 00 00 00  >................<
0000f0 00 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00  >................<
000100 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  >................<
*
000170 00 00 00 00 00 00 00 00 2e 66 6c 61 74 00 00 00  >.........flat...<
000180 04 00 00 00 00 10 00 00 00 02 00 00 00 02 00 00  >................<
000190 00 00 00 00 00 00 00 00 00 00 00 00 60 00 00 e0  >............`...<
0001a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  >................<
*
000200

이러한 헤더에 머신 코드를 부가하고, Windows 의 경우는, 파일 사이즈를 1024 바이트로 하기 위해서 늘 바이트를 부가하면, 대응하는 OS 로 동작하는 유효한 실행 파일을 얻을 수 있습니다.


  1. 프로그램이 어느 정도의 계산을 수행한 후 종료하려고 한다고 가정합니다.

    이제 두 가지 옵션이 있습니다.

    1. 크래시 - 예를 들어 비활성 명령 실행에 의해 (x86에서는 다음과 같은 경우가 있습니다.UD2OS에 의존하지 않고 간단하지만 우아하지는 않습니다.

    2. OS에 프로세스를 올바르게 종료하도록 의뢰합니다.현시점에서는, 이것을 실시하기 위한 OS 의존적인 메카니즘이 필요합니다.

x86 Linux에서는 다음과 같습니다.

xor ebx, ebx ; zero exit code
mov eax, 1   ; __NR_exit
int 0x80     ; do the system call (the easiest way)

x86 Windows 7 에서는, 다음과 같이 됩니다.

    ; First call terminates all threads except caller thread, see for details:
    ; http://www.rohitab.com/discuss/topic/41523-windows-process-termination/
    mov eax, 0x172  ; NtTerminateProcess_Wind7
    mov edx, terminateParams
    int 0x2e        ; do the system call
    ; Second call terminates current process
    mov eax, 0x172
    mov edx, terminateParams
    int 0x2e
terminateParams:
    dd 0, 0 ; processHandle, exitStatus

다른 Windows 버전에서는 다른 시스템 콜 번호가 필요합니다.적절한 전화 방법NtTerminateProcessOS 의존성이라는 또 다른 뉘앙스를 통해 공유 라이브러리를 구현하고 있습니다.


  1. 이제 프로그램이 일부 공유 라이브러리를 로드하여 휠을 재창조하는 것을 피하려고 합니다.

네, 실행 파일 형식이 다르다는 것을 확인했습니다.이를 고려하여 대상 OS 각각을 대상으로 하는 파일의 Import 섹션을 준비했다고 가정합니다.여전히 문제가 있습니다. OS마다 함수를 호출하는 방법(이른바 호출 규칙)이 다릅니다.

예를 들어 프로그램이 호출해야 하는 C 언어 함수가 두 개의 구조를 반환한다고 가정합니다.int가치.Linux에서 호출자는 다음과 같이 공간을 할당하고 호출되는 함수에 대한 첫 번째 파라미터로 포인터를 전달해야 합니다.

sub esp, 12 ; 4*2+alignment: stack must be 16-byte aligned
push esp    ;                right before the call instruction
call myFunc

Windows 에서는, 최초로int에서의 구조 가치EAX, 및 의 두 번째EDX추가 파라미터를 함수에 전달하지 않습니다.


다른 이름 망글링 방식(같은 OS에서도 컴파일러마다 다를 수 있음), 다른 데이터 유형(예:long doubleMSVC와long double컴파일러와 링커의 관점에서 OS의 가장 중요한 차이입니다.

「CPU」 「OS」 「CPU」 「OS」 「CPU」 「OS」.를 들어,를 해야 해 보겠습니다.coutcout최종적으로 프로그램이 실행되고 있는 OS의 API 함수를 호출하게 됩니다.이 호출은 운영 체제에 따라 다를 수 있습니다.따라서 OS별로 프로그램을 컴파일하여 올바른 OS 호출을 할 필요가 있습니다.

  1. 표준 라이브러리와 C 런타임은 OS API와 상호 작용해야 합니다.
  2. 대상 OS마다 실행 가능한 형식이 다릅니다.
  3. OS 커널에 따라 하드웨어를 다르게 구성할 수 있습니다.바이트 순서, 스택 방향, 레지스터 사용 규칙 등 많은 것들이 물리적으로 다를 수 있습니다.

엄밀히 말하면, 당신은 할 필요가 없다.

프로그램 로더

WSL1 또는 darling 와인이 있습니다.이것들은 모두 다른 OS의 바이너리 포맷용 로더입니다.기계가 기본적으로 같기 때문에 이 도구들은 매우 잘 작동합니다.

실행 파일을 생성할 때 "5+3"의 기계 코드는 기본적으로 모든 x86 기반 플랫폼에서 동일하지만 다음과 같은 다른 답변에서 이미 언급된 차이점이 있습니다.

  • 파일 형식
  • API: 예OS에 의해 공개되는 기능
  • ABI: 바이너리 레이아웃 등

이것들은 다르다.예를 들어, 와인은 Linux에 WinPE 포맷을 인식시키고, "단순히" 기계 코드를 Linux 프로세스로 실행합니다(에뮬레이션 없음).WinAPI의 일부를 구현하여 Linux용으로 변환합니다.실제로 Windows 프로그램도 Windows 커널(NT)과 통신하지 않고 Win32 서브시스템과 통신하기 때문에 Windows도 거의 같은 처리를 합니다.WinAPI를 NT API로 변환합니다.따라서 와인은 Linux API를 기반으로 한 또 다른 WinAPI 구현입니다.

VM 내의 C

또한 실제로 C를 LLVM 바이트 코드나 wasm과 같이 "나머지" 기계 코드 이외의 것으로 컴파일할 수 있습니다.GraalVM과 같은 프로젝트에서는 Java Virtual Machine에서 C를 실행할 수도 있습니다.컴파일 한 번, 모든 장소에서 실행.처음부터 "휴대성"을 의도한 다른 API/ABI/파일 형식을 대상으로 합니다.

따라서 ISA는 CPU가 인식할 수 있는 언어 전체를 구성하고 있지만 대부분의 프로그램은 CPU ISA에 의존할 뿐만 아니라 OS를 작동시켜야 합니다.툴 체인은 이를 고려해야 합니다.

하지만 당신이 옳아요.

하지만 사실, 당신이 옳다는 것에 꽤 가깝습니다.실제로 컴파일러를 사용하여 Linux 및 Win32용으로 컴파일할 수 있으며 컴파일러의 정의를 좁힐 수도 있습니다.단, 컴파일러를 다음과 같이 기동하면,

c99 -o foo foo.c

컴파일(C 코드를 어셈블리로 변환)할 뿐만 아니라 다음과 같은 작업을 수행합니다.

  1. C 프리프로세서를 실행합니다.
  2. 실제 C 컴파일러 프런트엔드를 실행합니다.
  3. 어셈블러를 실행합니다.
  4. 링커를 실행합니다.

어느 정도 단계가 있을 수 있지만, 그것이 일반적인 파이프라인입니다.그리고 2단계는 다시 말씀드리지만 기본적으로 모든 플랫폼에서 동일합니다.단, 프리프로세서는 다른 헤더파일을 컴파일 유닛에 카피하고(스텝 1) 링커는 전혀 다르게 동작합니다.어떤 언어(C)에서 다른 언어(ASM)로의 실제 번역은 이론적인 관점에서 컴파일러가 수행하는 것은 플랫폼에 의존하지 않습니다.

바이너리가 올바르게 기능하기 위해서는(또는 경우에 따라서는 전혀) 일관성이 필요하지만 이에 한정되지 않는 등 많은 추악한 세부 사항이 있습니다.

  • 프로시저 호출, 파라미터, 타입 등의 C 소스 구조가 레지스터, 메모리 위치, 스택프레임 등의 아키텍처 고유의 구성에 매핑되는 방법.
  • 바이너리 로더가 가상 주소 공간의 올바른 장소에 컴파일 결과를 로드하거나 임의 장소에 로드한 후 "픽스업"을 실행할 수 있도록 컴파일 결과를 실행 파일로 표현하는 방법.
  • 표준 라이브러리가 어떻게 구현되는지, 표준 라이브러리 함수는 라이브러리의 실제 함수일 수 있지만, 매크로, 인라인 함수 또는 라이브러리의 비표준 함수에 의존하는 컴파일러 빌트인 경우가 많습니다.
  • OS와 어플리케이션의 경계가 unix와 같은 시스템에서 C 표준 라이브러리는 핵심 플랫폼 라이브러리로 간주됩니다.한편 윈도에서는 C 표준 라이브러리는 컴파일러가 제공하는 것으로 간주되며 응용 프로그램에 컴파일되거나 함께 출하됩니다.
  • 다른 라이브러리는 어떻게 구현됩니까?그들은 어떤 이름을 사용하나요?어떻게 장전되어 있습니까?

이들 중 하나 이상의 차이가 있기 때문에 어떤 OS를 대상으로 한 바이너리를 다른 OS에 정상적으로 로드할 수 없습니다.

어떤 OS를 대상으로 한 코드를 다른 OS로 실행하는 것은 가능하다고 말했습니다.그것은 본질적으로 와인이 하는 일이다.Windows API 호출을 Linux에서 사용할 수 있는 호출로 변환하는 특별한 변환기 라이브러리와 Windows 및 Linux 바이너리를 모두 로드하는 방법을 알고 있는 특별한 바이너리 로더가 있습니다.

언급URL : https://stackoverflow.com/questions/61644911/why-do-you-need-to-recompile-c-c-for-each-os

반응형