분할 결함

Segmentation fault

컴퓨팅에서 분할 결함(흔히 segfault로 단축) 또는 액세스 위반은 소프트웨어가 제한된 메모리 영역(메모리 액세스 위반)에 액세스를 시도했음을 알리는 운영 체제(OS)에 의해 제기되는 결함 또는 고장 조건이다.표준 x86 컴퓨터에서 이것은 일반적인 보호 장애의 한 형태다.이에 대응하여 운영 체제 커널은 일반적으로 일부 수정 조치를 수행하며, 일반적으로 프로세스 신호를 전송하여 오류를 위반 프로세스에 전달한다.어떤 경우에는 프로세스가 사용자 지정 신호 처리기를 설치하여 스스로 복구할 수 있지만,[1] 그렇지 않으면 OS 기본 신호 처리기가 사용되어 일반적으로 프로세스가 비정상적으로 종료(프로그램 충돌)되고 때로는 코어 덤프가 발생하기도 한다.

분할 결함은 낮은 수준의 메모리 액세스를 제공하고 안전 점검을 거의 하지 않는 C와 같은 언어로 작성된 프로그램에서 흔히 발생하는 오류 유형이다.그것들은 주로 가상 메모리 어드레싱, 특히 불법 액세스에 대한 포인터의 사용 오류로 인해 발생한다.또 다른 유형의 메모리 액세스 오류는 다양한 원인이 있지만 오늘날에는 훨씬 드물다. 이러한 오류는 주로 잘못된 물리적 메모리 주소 지정 또는 잘못 정렬된 메모리 액세스 때문에 발생한다. 이러한 오류는 프로세스가 처리할 수 없는 참조가 아니라 하드웨어가 처리할 수 없는 메모리 참조다.

많은 프로그래밍 언어는 분할 결함을 피하고 메모리 안전을 개선하기 위해 고안된 메커니즘을 사용할 수 있다.예를 들어, 러스트 프로그래밍 언어는 소유권[2] 기반 모델을 사용하여 메모리 안전을 보장한다.[3]LispJava와 같은 다른 언어는 가비지 컬렉션을 사용하므로 분할 결함으로 이어질 수 있는 특정 등급의 메모리 오류를 방지한다.[4][5]

개요

사람이 생성한 신호의 예
윈도우즈 8null 포인터 참조

분할 오류는 프로그램에서 접근이 허용되지 않는 메모리 위치에 액세스를 시도하거나 허용되지 않는 방식으로 메모리 위치에 액세스를 시도할 때 발생한다(예: 읽기 전용 위치에 쓰기를 시도하거나 운영 체제의 일부를 덮어쓰려는 경우).

"분할"이라는 용어는 컴퓨팅에서 다양한 용도를 가지고 있으며, 1950년대 이후 사용된 용어인 "분할 결함"의 맥락에서 프로그램의 주소 공간을 가리킨다.[citation needed][6]메모리 보호 기능을 사용하면 프로그램 자체의 주소 공간만 읽을 수 있으며, 이 중 프로그램의 데이터 세그먼트있는 스택과 읽기/쓰기 부분만 쓰기 가능한 반면 읽기 전용 데이터와 코드 세그먼트는 쓰기 가능하지 않다.따라서 프로그램의 주소 공간 외부에 읽기를 시도하거나 주소 공간의 읽기 전용 세그먼트에 쓰기를 시도하면 분할 오류가 발생하므로 이름이 지정된다.

가상 메모리를 제공하기 위해 하드웨어 메모리 분할을 사용하는 시스템에서 하드웨어가 존재하지 않는 세그먼트를 참조하거나 세그먼트의 경계를 벗어난 위치를 참조하거나 해당 세그먼트에 부여된 권한에 의해 허용되지 않는 방식으로 위치를 참조하려는 시도를 감지할 때 분할 오류가 발생한다.페이징만 사용하는 시스템에서 잘못된 페이지 결함은 일반적으로 분할 결함으로 이어지며 분할 결함과 페이지 결함은 모두 가상 메모리 관리 시스템에 의해 제기된 결함으로 이어진다.분할 결함은 페이지 결함과 독립적으로 발생할 수 있다. 즉, 유효한 페이지에 대한 불법 액세스는 분할 결함은 아니지만, 페이지 결함은 유효하지 않은 페이지 결함은 아니며, 분할 결함은 페이지 내에 머무르지만 메모리를 불법적으로 덮어쓰는 버퍼 오버플로에서 페이지 중간에 발생할 수 있다(페이지 결함은 없음).

하드웨어 레벨에서, 메모리 보호 기능의 일부로서 (참조된 메모리가 존재하는 경우), 또는 유효하지 않은 페이지 결함(참조된 메모리가 존재하지 않는 경우)에 대해, MMU(메모리 관리 장치)에 의해 초기에 결함이 제기된다.문제가 잘못된 논리적 주소가 아니라 잘못된 물리적 주소일 경우, 버스 오류가 대신 제기되지만, 이러한 주소가 항상 구별되는 것은 아니다.

운영 체제 수준에서 이 고장이 잡히고 신호가 위반 프로세스에 전달되어 해당 신호에 대한 프로세스 처리기가 활성화된다.운영 체제마다 신호 이름이 달라 분할 결함이 발생했음을 표시한다.Unix와 유사한 운영 체제에서는 SIGSEGV(분할 위반에서 약칭)라는 신호가 위반 프로세스로 전송된다.Microsoft Windows에서 위반 프로세스는 STATUS_Access_VISION 예외를 받는다.

원인들

세분화 위반이 발생하는 조건과 이러한 위반이 발생하는 방법은 하드웨어와 운영 체제에 한정된다. 다른 하드웨어는 주어진 조건에 대해 서로 다른 결함을 발생시키고, 다른 운영 체제는 이를 프로세스에 전달되는 서로 다른 신호로 변환한다.근위 원인은 메모리 액세스 위반인 반면, 근본 원인은 일반적으로 어떤 종류의 소프트웨어 버그다.근본 원인(버그 디버깅)을 결정하는 것은 어떤 경우에는 간단할 수 있는데, 어떤 경우에는 프로그램이 일관되게 분할 결함을 야기할 수 있는 반면, 다른 경우에는 버그가 번식이 어렵고 각 실행의 메모리 할당에 의존할 수 있다(예: 매달린 포인터 참조).

다음은 분할 결함의 몇 가지 일반적인 원인이다.

  • 존재하지 않는 메모리 주소(외부 프로세스의 주소 공간) 액세스 시도 중
  • 프로그램에 대한 권한이 없는 메모리 액세스를 시도하는 중(프로세스 컨텍스트의 커널 구조 등)
  • 읽기 전용 메모리(코드 세그먼트 등) 쓰기 시도 중

이러한 오류는 종종 잘못된 메모리 액세스를 초래하는 프로그래밍 오류로 인해 발생한다.

  • 일반적으로 프로세스 주소 공간의 일부가 아닌 주소를 가리키는 null 포인터 비참조
  • 초기화되지 않은 포인터(임의 메모리 주소를 가리키는 와일드 포인터)에 비참조 또는 할당
  • 해제된 포인터(자유/할당/삭제된 메모리를 가리키는 당김 포인터)에 취소 또는 할당
  • 버퍼 오버플로
  • 스택 오버플로
  • 올바르게 컴파일되지 않는 프로그램 실행 시도. (일부 컴파일러는[which?] 컴파일 시간 오류가 발생함에도 불구하고 실행 파일을 출력한다.)

C 코드에서 분할 결함은 특히 C 동적 메모리 할당에서 포인터 사용 오류로 인해 가장 자주 발생한다.정의되지 않은 동작이 발생하는 null 포인터를 무시하면 일반적으로 분할 오류가 발생한다.이는 null 포인터가 유효한 메모리 주소가 될 수 없기 때문이다.반면에 야생 포인터와 매달린 포인터들은 존재할 수도 있고 없을 수도 있는 기억을 가리키며, 읽을 수도 있고 쓸 수도 없을 수도 있고, 따라서 일시적인 버그를 초래할 수 있다.예를 들면 다음과 같다.

마를 뜨다 *p1 = NULL;           // Null 포인터 마를 뜨다 *p2;                  // 와일드 포인터: 전혀 초기화되지 않음. 마를 뜨다 *p3  = 만록의(10 * 의 크기(마를 뜨다));  // 할당된 메모리에 대한 포인터가 초기화됨                                         // (일반적으로 malloc는 실패하지 않음) 무료의(p3);                  // 이제 메모리가 해제되었으므로 p3는 매달린 포인터가 된다. 

이러한 변수 중 하나를 무시하면 분할 오류가 발생할 수 있다. null 포인터를 무시하면 일반적으로 Segfault가 발생할 수 있는 반면, wild pointer에서 판독하면 대신 랜덤 데이터가 생성되지만 segfault가 없을 수 있으며, 매달린 포인터에서 판독하면 한동안 유효한 데이터가 생성되고, 이후 데이터를 덮어쓸 때 랜덤 데이터가 생성될 수 있다.

처리

분할 결함 또는 버스 오류에 대한 기본 동작은 분할 오류를 트리거한 프로세스의 비정상 종료다.디버깅을 지원하기 위해 코어 파일이 생성될 수 있으며, 다른 플랫폼 종속 작업도 수행할 수 있다.예를 들어, grsecurity 패치를 사용하는 Linux 시스템은 버퍼 오버플로우를 사용하여 가능한 침입 시도를 모니터하기 위해 SIGSEGV 신호를 기록할 수 있다.

Linux와 Windows와 같은 일부 시스템에서는 프로그램 자체가 분할 장애를 처리할 수 있다.[7]그 구조 운영 체제에 따라 상영 프로그램 뿐만 아니라는 스택 추적 프로세서 레지스터 값과 소스 코드의 시작된는 라인을 타당하지 않게 존재인지, 그 행동은 읽기 또는 쓰기 accessed[8]다 기억 주소를 맺는 것 같은 국가에 대한 정보를 추출할 수 있는 이벤트를 처리할 수 없다.[9]

분할 결함은 일반적으로 프로그램에 수정이 필요한 버그가 있다는 것을 의미하지만, 테스트, 디버깅을 목적으로 의도적으로 그러한 장애를 유발하고 메모리에 대한 직접 액세스가 필요한 플랫폼을 에뮬레이트하는 것도 가능하다.후자의 경우, 시스템은 고장이 발생한 후에도 프로그램을 실행할 수 있어야 한다.이 경우, 시스템이 허용했을 때, 실행을 계속하기 위한 실패한 지침에 대해 이벤트를 처리하고 프로세서 프로그램 카운터를 "점프"로 증가시킬 수 있다.[10]

EMV 키패드의 분할 결함

읽기 전용 메모리에 쓰기

읽기 전용 메모리에 기록하면 분할 오류가 발생한다.코드 오류 수준에서 이는 OS에 의해 읽기 전용 메모리에 로드되기 때문에 프로그램이 자체 코드 세그먼트일부 또는 데이터 세그먼트의 읽기 전용 부분에 쓸 때 발생한다.

메모리 보호 기능이 있는 플랫폼에서 일반적으로 분할 장애를 일으키는 ANSI C 코드의 예가 여기에 있다.ANSI C 표준에 따라 정의되지 않은 동작인 문자열 리터럴의 수정을 시도한다.대부분의 컴파일러는 컴파일할 때 이를 포착하지 못하고 대신 실행 가능한 코드로 컴파일하여 다음과 같이 충돌한다.

인트로 본래의(공허하게 하다) {     마를 뜨다 *s = "헬로 월드";     *s = 'H'; } 

이 코드가 포함된 프로그램을 컴파일하면 프로그램 실행 파일로다타 섹션(데이터 세그먼트의 읽기 전용 섹션)에 "헬로 월드"라는 문자열이 배치된다.로드되면 운영 체제는 다른 문자열과 상수 데이터를 메모리의 읽기 전용 세그먼트에 배치한다.실행되면 변수 s가 문자열의 위치를 가리키도록 설정되며, 변수를 통해 H 문자를 메모리에 쓰려고 시도하여 분할 오류가 발생한다.이러한 프로그램을 컴파일할 때 읽기 전용 위치 할당을 확인하지 않는 컴파일러로 컴파일하여 Unix와 유사한 운영 체제에서 실행하면 다음과 같은 런타임 오류가 발생한다.

$gcc segfault.c -g -o segfault $ ./segfault Segmentation 오류

GDB에서 코어 파일의 역추적:

프로그램 받았다 신호를 보내다 시그세그브, 분할 과실을 대다. 0x1c0005c2  본래의 () 에서 결점.c:6 6               *s = 'H'; 

이 코드는 스택에 메모리를 할당하고 문자열 리터럴 값으로 초기화하므로 문자 포인터 대신 배열을 사용하여 수정할 수 있다.

마를 뜨다 s[] = "헬로 월드"; s[0] = 'H';  // 동등하게, *s = 'H'; 

문자열 리터럴은 수정되어서는 안되지만(이것은 C 표준에서 정의되지 않은 동작을 가지고 있다), C에서 문자열 리터럴은 다음과 같다.static char []따라서 원래 [11][12][13]코드에는 암묵적인 변환이 없다(이 변환은 a를 가리킨다).char *C++에 있는 동안static const char []유형, 따라서 암묵적 변환이 존재하므로 컴파일러는 일반적으로 이 특정 오류를 포착할 것이다.

Null 포인터 참조 해제

C 및 C 유사 언어에서 null 포인터는 "아무 개체로의 포인터"를 의미하고 오류 표시기로 사용되며, null 포인터를 통한 null 포인터(null pointer를 통한 읽기 또는 쓰기)는 매우 일반적인 프로그램 오류다.C 표준은 null 포인터가 메모리 주소 0에 대한 포인터와 동일하다고 말하지 않는다. 그러나 실제로는 그럴 수 있다.대부분의 운영 체제는 null 포인터의 주소를 매핑하여 액세스하면 분할 오류가 발생하도록 한다.이러한 행동은 C기준에 의해 보장되지 않는다.Null 포인터 비참조는 C에서 정의되지 않은 동작이며, 적절한 구현은 참조되지 않은 포인터가 Null이 아니라고 가정할 수 있다.

인트로 *삐걱삐걱거리다 = NULL; 활자화하다("%d", *삐걱삐걱거리다); 

이 샘플 코드는 null 포인터를 만든 다음 그 값에 액세스하려고 한다(값을 읽는다).그렇게 하면 많은 운영 체제에서 런타임에 분할 오류가 발생한다.

null 포인터를 취소한 다음 해당 포인터에 할당(존재하지 않는 대상에 값을 기록)하는 경우에도 일반적으로 분할 오류가 발생한다.

인트로 *삐걱삐걱거리다 = NULL; *삐걱삐걱거리다 = 1; 

다음 코드는 null 포인터 참조를 포함하지만, 컴파일할 때 값이 사용되지 않기 때문에 종종 분할 오류가 발생하지 않으며, 따라서 데드 코드 제거로 인해 참조 거부가 최적화되는 경우가 많다.

인트로 *삐걱삐걱거리다 = NULL; *삐걱삐걱거리다; 

버퍼 오버플로

다음 코드가 문자 배열에 액세스s그 상공을 벗어나서컴파일러와 프로세서에 따라 분할 결함이 발생할 수 있다.

마를 뜨다 s[] = "헬로 월드"; 마를 뜨다 c = s[20]; 

스택 오버플로우

또 다른 예로는 베이스 케이스가 없는 재귀가 있다.

인트로 본래의(공허하게 하다) {     돌아오다 본래의(); } 

스택이 오버플로되어 분할 오류가 발생하는 경우.[14]무한 재귀로 언어, 컴파일러가 수행하는 최적화 및 코드의 정확한 구조에 따라 스택 오버플로가 반드시 발생하는 것은 아니다.이 경우 연결할 수 없는 코드(반환문)의 동작이 정의되지 않기 때문에 컴파일러는 이를 제거하고 스택이 사용되지 않을 수 있는 테일 콜 최적화를 사용할 수 있다.다른 최적화에는 반복을 반복으로 변환하는 것이 포함될 수 있으며, 예제 기능의 구조를 고려할 경우 프로그램이 영구적으로 실행되지만 스택은 넘치지 않을 수 있다.

참고 항목

참조

  1. ^ 전문가 C 프로그래밍: Peter Van der Linden, 188페이지의 깊은 C 비밀
  2. ^ "The Rust Programming Language - Ownership".
  3. ^ "Fearless Concurrency with Rust - The Rust Programming Language Blog".
  4. ^ McCarthy, John (April 1960). "Recursive functions of symbolic expressions and their computation by machine, Part I". Communications of the ACM. 4 (3): 184–195. doi:10.1145/367177.367199. S2CID 1489409. Retrieved 2018-09-22.
  5. ^ Dhurjati, Dinakar; Kowshik, Sumant; Adve, Vikram; Lattner, Chris (1 January 2003). "Memory Safety Without Runtime Checks or Garbage Collection" (PDF). Proceedings of the 2003 ACM SIGPLAN Conference on Language, Compiler, and Tool for Embedded Systems. ACM. 38 (7): 69–80. doi:10.1145/780732.780743. ISBN 1581136471. S2CID 1459540. Retrieved 2018-09-22.
  6. ^ "Debugging Segmentation Faults and Pointer Problems - Cprogramming.com". www.cprogramming.com. Retrieved 2021-02-03.
  7. ^ "Cleanly recovering from Segfaults under Windows and Linux (32-bit, x86)". Retrieved 2020-08-23.
  8. ^ "Implementation of the SIGSEGV/SIGABRT handler which prints the debug stack trace". GitHub. Retrieved 2020-08-23.
  9. ^ "How to identify read or write operations of page fault when using sigaction handler on SIGSEGV?(LINUX)". Retrieved 2020-08-23.
  10. ^ "LINUX – WRITING FAULT HANDLERS". Retrieved 2020-08-23.
  11. ^ "6.1.4 String literals". ISO/IEC 9899:1990 - Programming languages -- C.
  12. ^ "6.4.5 String literals". ISO/IEC 9899:1999 - Programming languages -- C.
  13. ^ "6.4.5 String literals". ISO/IEC 9899:2011 - Programming languages -- C.
  14. ^ 분할 오류와 스택 오버플로 간의 차이점은?스택 오버플로에서

외부 링크