네이티브 머신 코드를 쉽게 디 컴파일 할 수없는 이유는 무엇입니까?


16

Java, VB.NET, C #, ActionScript 3.0 등과 같은 바이트 코드 기반 가상 머신 언어를 사용하면 인터넷에서 디 컴파일러를 다운로드하고 바이트 코드를 한 번에 실행하는 것이 얼마나 쉬운 지에 대해 종종 듣습니다. 종종 몇 초 만에 원본 소스 코드와 너무 멀지 않은 것을 생각해냅니다. 아마도 이런 종류의 언어는 특히 그것에 취약합니다.

최근에 원래 이진 코드가 원래 작성된 언어 (및 디 컴파일하려는 언어)를 알았을 때 네이티브 이진 코드와 관련하여 더 이상 듣지 못하는 이유가 궁금해졌습니다. 오랫동안 네이티브 머신 언어가 일반적인 바이트 코드보다 훨씬 더 복잡하고 더 복잡하기 때문이라고 생각했습니다.

그러나 바이트 코드는 어떻게 생겼습니까? 다음과 같이 보입니다 :

1000: 2A 40 F0 14
1001: 2A 50 F1 27
1002: 4F 00 F0 F1
1003: C9 00 00 F2

그리고 네이티브 머신 코드는 어떻게 보입니까 (16 진수)? 물론 다음과 같습니다.

1000: 2A 40 F0 14
1001: 2A 50 F1 27
1002: 4F 00 F0 F1
1003: C9 00 00 F2

그리고 지침은 다소 비슷한 마음의 틀에서 나옵니다.

1000: mov EAX, 20
1001: mov EBX, loc1
1002: mul EAX, EBX
1003: push ECX

따라서 C ++과 같이 네이티브 바이너리를 디 컴파일하려고하는 언어가 어떻습니까? 즉시 생각 나는 유일한 두 가지 아이디어는 1) 실제로 바이트 코드보다 훨씬 복잡하다는 것입니다. 또는 2) 운영 체제가 프로그램을 페이지 매김하고 조각을 흩어 버리는 경향이 너무 많은 문제가 있다는 사실에 관한 것입니다. 그러한 가능성 중 하나가 맞다면 설명하십시오. 그러나 어느 쪽이든, 왜 기본적으로 이것을 듣지 못합니까?

노트

답 중 하나를 받아들이려고하지만 먼저 무언가를 언급하고 싶습니다. 거의 모든 사람들이 서로 다른 원본 소스 코드가 동일한 머신 코드에 매핑 될 수 있다는 사실을 다시 언급하고 있습니다. 로컬 변수 이름이 손실되고 원래 사용 된 루프 유형 등을 알 수 없습니다.

그러나 방금 언급 한 두 가지 예는 내 눈에 사소한 것입니다. 그러나 일부 답변은 머신 코드와 원본 소스의 차이가이 사소한 것보다 훨씬 더 크다고 진술하는 경향이 있습니다.

그러나 예를 들어 로컬 변수 이름 및 루프 유형과 같은 경우 바이트 코드에서도이 정보가 손실됩니다 (적어도 ActionScript 3.0의 경우). 전에는 디 컴파일러를 통해 그 내용을 가져 왔으며 변수가 호출되었는지 strMyLocalString:String또는 인지는 신경 쓰지 않았습니다 loc1. 나는 여전히 그 작은 지역 범위를 보았고 그것이 큰 문제없이 어떻게 사용되고 있는지 볼 수있었습니다. 그리고 for루프는while당신이 그것에 대해 생각하면 루프. 또한 irrFuscator (secureSWF와 달리 멤버 변수 및 함수 이름을 무작위 화하는 것 이상을 수행하지 않음)를 통해 소스를 실행할 때도 특정 변수 및 함수를 더 작은 클래스에서 분리하기 시작할 수있는 것처럼 보입니다. 그들이 어떻게 사용되는지, 그들 자신의 이름을 할당하고 거기서부터 일하십시오.

이것이 큰 문제가 되려면 머신 코드는 그것보다 훨씬 많은 정보를 잃어 버릴 필요가 있으며, 그 중 일부는 이것에 관한 것입니다.


35
햄버거로 소를 만드는 것은 어렵습니다.
Kaz Dragon

4
가장 큰 문제는 네이티브 바이너리가 프로그램에 대한 메타 데이터를 거의 보유하지 않는다는 것입니다. 클래스에 대한 정보 (C ++은 특히 디 컴파일하기 어렵게 함)를 유지하지 않으며 함수에 대해서도 항상 그런 것은 아닙니다. CPU는 본질적으로 한 번에 한 명령 씩 상당히 선형적인 방식으로 코드를 실행하기 때문에 필요하지 않습니다. 또한 코드와 데이터를 구분하는 것은 불가능합니다 ( link ). 자세한 정보는 RE.SE 에서 검색하거나 다시 요청하는 것이 좋습니다 .
ntoskrnl 2012

답변:


39

모든 컴파일 단계에서 복구 할 수없는 정보가 손실됩니다. 원본 소스에서 잃어버린 정보가 많을수록 디 컴파일하기가 더 어렵습니다.

최종 대상 기계 코드를 생성 할 때 보존되는 것보다 훨씬 많은 정보가 원래 소스에서 보존되므로 바이트 코드에 유용한 디 컴파일러를 작성할 수 있습니다.

컴파일러의 첫 단계는 소스를 트리로 표현되는 중간 표현을 위해 소스로 전환하는 것입니다. 일반적으로이 트리에는 주석, 공백 등과 같은 의미가없는 정보가 포함되어 있지 않습니다. 일단 폐기되면 해당 트리에서 원본 소스를 복구 할 수 없습니다.

다음 단계는 트리를 어떤 형태의 중간 언어로 렌더링하여 최적화를 쉽게하는 것입니다. 여기에는 몇 가지 선택 사항이 있으며 각 컴파일러 인프라에는 고유 한 옵션이 있습니다. 그러나 일반적으로 로컬 변수 이름, 큰 제어 흐름 구조 (예 : for 또는 while 루프 사용 여부)와 같은 정보가 손실됩니다. 일정한 전파, 변하지 않는 코드 모션, 함수 인라이닝 등의 중요한 최적화가 여기에서 발생합니다. 각각은 동등한 기능을 갖지만 실질적으로 다른 표현으로 표현을 변환합니다.

그 후 한 단계는 공통 명령어 패턴의 최적화 된 버전을 생성하는 "peep-hole"최적화를 포함 할 수있는 실제 기계 명령어를 생성하는 것입니다.

각 단계에서 결국에는 원래 코드와 유사한 것을 복구 할 수 없게 될 때까지 점점 더 많은 정보를 잃게됩니다.

반면 바이트 코드는 일반적으로 대상 기계 코드가 생성 될 때 JIT 단계 (Just-in-Time 컴파일러)까지 흥미롭고 혁신적인 최적화를 저장합니다. 바이트 코드에는 로컬 변수 유형, 클래스 구조와 같은 많은 메타 데이터가 포함되어있어 동일한 바이트 코드를 여러 대상 머신 코드로 컴파일 할 수 있습니다. 이 모든 정보는 C ++ 프로그램에서 필요하지 않으며 컴파일 프로세스에서 삭제됩니다.

다양한 대상 기계 코드에 대한 디 컴파일러가 있지만 원본 소스가 너무 많이 손실되어 유용한 결과 (수정 한 후 다시 컴파일 할 수있는 결과)를 생성하지 않는 경우가 많습니다. 실행 파일에 대한 디버그 정보가 있으면 더 나은 작업을 수행 할 수 있습니다. 그러나 디버그 정보가있는 경우 원래 소스도있을 수 있습니다.


5
JIT가 더 잘 작동 할 수 있도록 정보가 유지된다는 사실이 핵심입니다.
btilly

그렇다면 C ++ DLL은 쉽게 컴파일 할 수 있습니까?
Panzercrisis

1
내가 유용하다고 생각하는 것은 아닙니다.
chuckj 2012

1
메타 데이터는 "동일한 바이트 코드를 여러 대상으로 컴파일 할 수 있도록"하는 것이 아니라 반영하기위한 것입니다. 대상 변경 가능한 중간 표현에는 해당 메타 데이터가 필요하지 않습니다.
SK-logic

2
사실이 아닙니다. 많은 데이터가 리플렉션을 위해 존재하지만 리플렉션 만 사용되는 것은 아닙니다. 예를 들어, 인터페이스 및 클래스 정의는 대상 시스템에서 필드 오프셋 정의, 가상 테이블 구성 등을 생성하여 대상 시스템에 가장 효율적인 방식으로 구성 될 수 있도록하는 데 사용됩니다. 이 테이블은 원시 코드를 생성 할 때 컴파일러 및 / 또는 링커에 의해 구성됩니다. 이 작업이 완료되면이를 구성하는 데 사용 된 데이터가 삭제됩니다.
chuckj

11

다른 답변에서 지적한 바와 같이 정보 손실은 한 가지 점이지만 거래를 중단하는 것은 아닙니다. 결국, 당신은 원하는, 원래의 프로그램 등을 기대하지 않습니다 어떤 높은 수준의 언어로 표현. 코드가 인라인 된 경우 코드를 그대로 두거나 일반적인 계산을 자동으로 제거 할 수 있습니다. 원칙적으로 많은 최적화를 취소 할 수 있습니다. 그러나 원칙적으로 돌이킬 수없는 일부 작업이 있습니다 (적어도 무한한 양의 컴퓨팅이없는 경우).

예를 들어, 분기는 계산 된 점프가 될 수 있습니다. 다음과 같은 코드 :

select (x) {
case 1:
    // foo
    break;
case 2:
    // bar
    break;
}

에 컴파일 될 수 있습니다 (실제 어셈블러가 아님).

0x1000:   jump to 0x1000 + 4*x
0x1004:   // foo
0x1008:   // bar
0x1012:   // qux

이제 x가 1 또는 2 일 수 있다는 것을 알고 있다면 점프를보고 이것을 쉽게 바꿀 수 있습니다. 그러나 주소 0x1012는 어떻습니까? 당신 case 3도 그것을 만들어야 합니까? 어떤 값이 허용되는지 파악하려면 최악의 경우 전체 프로그램을 추적해야합니다. 더 나쁜 것은 가능한 모든 사용자 입력을 고려해야 할 수도 있습니다! 문제의 핵심은 데이터와 지침을 구분할 수 없다는 것입니다.

즉, 전적으로 비관적이지는 않을 것입니다. 위의 '어셈블러'에서 알 수 있듯이 x가 외부에서 나오고 1 또는 2로 보장 되지 않는 경우 본질적으로 어디에서나 이동할 수있는 나쁜 버그가 있습니다. 그러나 프로그램에 이런 종류의 버그가 없다면 추론하기가 훨씬 쉽습니다. (CLR IL 또는 Java 바이트 코드와 같은 "안전한"중간 언어가 메타 데이터를 제쳐두고도 디 컴파일하기가 훨씬 쉽다는 것은 우연이 아닙니다. 따라서 실제로는 잘 작동하는 특정 디 컴파일이 가능해야합니다.프로그램들. 부작용과 잘 정의 된 입력이없는 개별적이고 기능적인 스타일 루틴을 생각하고 있습니다. 간단한 함수에 의사 코드를 제공 할 수있는 두 개의 디 컴파일러가 있다고 생각하지만 그러한 도구에 대한 경험이 많지 않습니다.


9

머신 코드를 원래 소스 코드로 쉽게 변환 할 수없는 이유는 컴파일 중에 많은 정보가 손실되기 때문입니다. 메소드와 익스포트되지 않은 클래스는 인라인 될 수 있고, 로컬 변수 이름이 손실되고, 파일 이름과 구조가 완전히 손실되며, 컴파일러는 명백하지 않은 최적화를 수행 할 수 있습니다. 또 다른 이유는 여러 다른 소스 파일이 정확히 동일한 어셈블리를 생성 할 수 있기 때문입니다.

예를 들면 다음과 같습니다.

int DoSomething()
{
    return Add(5, 2);
}

int Add(int x, int y)
{
    return x + y;
}

int main()
{
    return DoSomething();
}

다음과 같이 컴파일 할 수 있습니다.

main:
mov eax, 7;
ret;

내 어셈블리는 꽤 녹슨 편이지만 컴파일러에서 최적화를 정확하게 수행 할 수 있는지 확인할 수 있으면 그렇게 할 수 있습니다. 이것은 컴파일 된 바이너리가 이름을 알 필요 없습니다 때문입니다 DoSomethingAdd뿐만 아니라 사실 Add방법은 두 개의 명명 된 매개 변수를 가지고, 컴파일러는 또한 것을 알고있다 DoSomething방법은 기본적으로 상수를 반환하고, 메서드 호출과 모두 인라인 수 방법 자체.

컴파일러의 목적은 소스 파일을 묶는 방법이 아닌 어셈블리를 만드는 것입니다.


마지막 명령을 그냥 변경 ret하고 C 호출 규칙을 가정한다고 가정하십시오.
chuckj February

3

여기서 일반적인 원칙은 일대일 매핑이며 표준 대표가 부족합니다.

다 대일 현상의 간단한 예를 들어, 일부 지역 변수가있는 함수를 사용하여 기계 코드로 컴파일 할 때 발생하는 상황에 대해 생각할 수 있습니다. 변수에 대한 모든 정보는 메모리 주소가되기 때문에 유실됩니다. 루프에서도 비슷한 일이 발생합니다. for또는 while루프를 사용할 수 있으며 바로 구성되어 있으면 jump지침 과 동일한 머신 코드를 얻을 수 있습니다 .

또한 머신 코드 명령어의 원래 소스 코드에서 표준 대표자가 부족합니다. 루프를 디 컴파일하려고 할 때 jump명령어를 루프 구성에 어떻게 다시 매핑 합니까? 그것들을 for루프 또는 while루프로 만드십시오 .

현대 컴파일러가 다양한 형태의 폴딩 및 인라인을 수행한다는 사실로 인해 문제가 더욱 악화됩니다. 따라서 머신 코드에 도달 할 때까지는 로우 레벨 머신 코드의 출처를 알 수 없습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.