itsource

C는 포인터가 참조되지 않은 상태에서 포인터가 경계를 벗어났는지 확인합니까?

mycopycode 2023. 8. 5. 10:07
반응형

C는 포인터가 참조되지 않은 상태에서 포인터가 경계를 벗어났는지 확인합니까?

저는 C의 경계를 벗어난 포인터가 무시되지 않더라도 정의되지 않은 동작을 일으킨다고 말하는 사람들과 이 논쟁을 했습니다.예:

int a;
int *p = &a;
p = p - 1;

여기서 세 번째 줄은 다음과 같은 경우에도 정의되지 않은 동작을 유발합니다.p무시되지 .*p사용되지 않음).

제 생각에는, C가 포인터를 사용하지 않고 포인터가 경계 밖에 있는지 확인하는 것은 비논리적으로 들립니다(마치 누군가가 그의 집에 들어올 경우에 대비하여 거리의 사람들이 총을 소지하고 있는지 검사하는 것과 같습니다).가장 이상적인 것은 사람들이 집에 들어가려고 할 때 그들을 검사하는 것입니다.)저는 C가 그것을 확인한다면 많은 런타임 오버헤드가 발생할 것이라고 생각합니다.

또한 C가 정말 OOB 포인터를 확인한다면 UB가 발생하지 않는 이유는 무엇입니까?

int *p; // uninitialized thus pointing to a random adress

이 경우에는 가능성이 있더라도 아무 일도 일어나지 않는 이유가 무엇입니까?pOOB 주소를 가리키는 것은 높음입니다.

추가:

int a;
int *p = &a;
p = p - 1;

말합니다&a1000입니다.의가는의 요?p한 후에는 다음과 같이 됩니다

  • 996 하지만 여전히 정의되지 않은 행동은 다음과 같습니다.p다른 곳에서 불합격 처리되어 진짜 문제를 일으킬 수 있습니다.
  • 정의되지 않은 값이 정의되지 않은 동작입니다.

왜냐하면 애초에 "세 번째 줄은 정의되지 않은 동작으로 호출되었다"는 것은 미래에 OOB 포인터(디레퍼런스)를 사용할 가능성이 있기 때문이고 사람들은 시간이 지남에 따라 이를 정의되지 않은 동작으로 받아들였기 때문입니다. 이제, 의는의 은?p100% 996이 될 것이고 여전히 정의되지 않은 동작 또는 그 값이 정의되지 않을 것입니까?

C는 포인터가 범위를 벗어났는지 확인하지 않습니다.그러나 예외가 되는 개체의 끝 바로 뒤를 가리키며 개체 경계 밖에 있는 주소가 계산될 때 기본 하드웨어가 이상한 방식으로 동작할 수 있습니다.C 표준은 이를 정의되지 않은 동작을 유발하는 것으로 명시적으로 설명합니다.

대부분의 현재 환경에서 위의 코드는 문제를 일으키지 않지만 유사한 상황이 25년 전 x86 16비트 보호 모드에서 분할 오류를 발생시킬 수 있습니다.

표준 언어에서 이러한 값은 정의되지 않은 동작을 호출하지 않고는 조작할 수 없는 트랩 값일 수 있습니다.

C11 표준의 관련 섹션은 다음과 같습니다.

6.5.6 가법 연산자

  1. 정수 유형의 식을 포인터에 추가하거나 포인터에서 빼면 결과는 포인터 피연산자 유형을 가집니다.포인터 피연산자가 배열 개체의 요소를 가리키고 배열이 충분히 크면 결과 배열 요소와 원래 배열 요소의 첨자 차이가 정수 식과 같도록 원래 요소의 오프셋을 가리킵니다. [...] 포인터 피연산자와 결과가 모두 다음 요소를 가리킨다면동일한 배열 개체 또는 배열 개체의 마지막 요소를 지나면 평가에서 오버플로가 발생하지 않습니다. 그렇지 않으면 동작이 정의되지 않습니다.객체의 , 결가배 을과개킬 1경가우리, ▁a,▁object▁oper▁of▁ifary▁다▁the▁points▁as▁result▁of▁array▁un▁be의 피연산자로 할 수 없습니다.*계산되는 연산자입니다.

정의되지 않은 동작의 유사한 예는 다음과 같습니다.

char *p;
char *q = p;

되지 않은 포인터의 .p정의되지 않은 동작을 호출합니다. 이 동작은 무시되지 않습니다.

편집: 이것에 대해 논쟁하려는 것은 논쟁거리입니다.표준에서는 이러한 주소를 계산하면 정의되지 않은 동작이 발생한다고 말합니다.일부 구현에서는 일부 값을 계산하여 저장할 수도 있고 저장할 수도 있다는 사실은 중요하지 않습니다.정의되지 않은 동작에 대한 가정에 의존하지 마십시오. 컴파일러는 본질적으로 예측할 수 없는 특성을 이용하여 상상할 수 없는 최적화를 수행할 수 있습니다.

예를 들어 이 루프는 다음과 같습니다.

for (int i = 1; i != 0; i++) {
    ...
}

할 수 .i++는 동작을 합니다.i이라INT_MAX컴파일러의 분석은 다음과 같습니다.

  • 의 초 기가의 i이라> 0.
  • 의 어떤 긍정적인 가치에도 불구하고i < INT_MAX,i++ 직도입니다.> 0
  • 위해서i = INT_MAX,i++정의되지 않은 행동을 유발하므로, 우리는 그것을 추측할 수 있습니다.i > 0우리가 원하는 모든 것을 가정할 수 있기 때문입니다.

그므로러.i 상항입니다.> 0테스트 코드를 제거할 수 있습니다.

실제로, C 프로그램이 요소에 대한 포인터 또는 동일한 배열 요소의 끝을 지나는 포인터에 결과를 초래하지 않는 포인터 산술을 통해 값을 계산하려고 시도하는 경우 C 프로그램의 동작은 정의되지 않습니다.C116.5.6/8에서 시작:

포인터 피연산자와 결과가 모두 동일한 배열 개체의 요소를 가리키거나 배열 개체의 마지막 요소를 지난 요소를 가리킬 경우 평가에서 오버플로가 발생하지 않습니다. 그렇지 않으면 동작이 정의되지 않습니다.

설명의 , 의 주소 (이설의목위을해적, 객주소의는체의형유명▁of▁(주▁the이는▁of▁address소▁(▁an▁type▁object객의,체forT의 첫 될 수 .T[1].)

명확하게 설명하자면, "정의되지 않은 행동"은 해당 코드의 결과가 언어를 지배하는 표준에서 정의되지 않음을 의미합니다.실제 결과는 컴파일러가 구현되는 방식에 따라 달라지며, 전혀 없는 상태에서 완전한 충돌 및 그 사이의 모든 것에 이르기까지 다양합니다.

표준은 포인터의 범위 검사를 수행해야 한다고 지정하지 않습니다.하지만 당신의 구체적인 예와 관련하여, 그들은 다음과 같이 말합니다.

정수 유형의 식을 포인터에 추가하거나 포인터에서 빼면...포인터 피연산자와 결과가 모두 동일한 배열 개체의 요소를 가리키거나 배열 개체의 마지막 요소를 지난 요소를 가리킬 경우 평가에서 오버플로가 발생하지 않습니다. 그렇지 않으면 동작이 정의되지 않습니다.결과가 배열 개체의 마지막 요소를 하나 지나도록 가리킬 경우, 평가되는 단항 * 연산자의 피연산자로 사용할 수 없습니다.

위의 인용문은 C99 § 6.5.6 패러 8 (제가 가지고 있는 최신 버전)의 것입니다.

위의 내용은 비배열 포인터에도 적용됩니다. 이전 절에서 다음과 같이 지정되어 있기 때문입니다.

이러한 연산자의 목적을 위해 배열의 요소가 아닌 개체에 대한 포인터는 개체 유형을 요소 유형으로 하는 길이가 1인 배열의 첫 번째 요소에 대한 포인터와 동일하게 동작합니다.

따라서 포인터 산술을 수행하고 결과가 개체의 끝을 지나 범위 내에 있거나 하나를 가리키면 유효한 결과를 얻을 수 있습니다. 그렇지 않으면 정의되지 않은 동작을 얻을 수 있습니다.그 행동은 결국 포인터가 빗나간 것일 수도 있지만, 다른 것일 수도 있습니다.

예, 포인터가 참조되지 않더라도 정의되지 않은 동작입니다.

C에서는 포인터가 배열 경계를 지나 하나의 요소만 가리킬 수 있습니다.

사양에 정의되지 않은 것이 있다고 하면 상당히 혼란스러울 수 있습니다.

그런 상황에서 명세서의 이행은 자유롭게 마음대로 할 수 있다는 뜻입니다.어떤 경우에는, 직관적으로 옳은 것처럼 보이는 것을 할 것입니다.그렇지 않은 경우도 있습니다.

주소 경계 사양의 경우, 저는 제 직관이 평평하고 균일한 메모리 모델에 대한 가정에서 나온다는 것을 알고 있습니다.하지만 다른 메모리 모델도 있습니다.

"undefined"라는 단어는 의도치 않게 완료된 사양에 표시되지 않습니다.표준 위원회는 일반적으로 표준의 다른 구현이 다른 작업을 수행해야 한다는 것을 알고 있을 때 이 단어를 사용하기로 결정합니다.대부분의 경우 서로 다른 이유는 성능 때문입니다.그래서: 스펙에 있는 단어의 출현은 우리에게 단지 인간, 스펙의 사용자들에게 우리의 직관이 틀릴 수도 있다는 경고입니다.

이런 종류의 "원하는 대로" 사양은 몇 년 전에 짜증이 난 것으로 유명합니다.그래서, 그는 그의 그누 컴파일러 모음(gcc)의 일부 버전이 정의되지 않은 무언가를 만났을 때 컴퓨터 게임을 시도하도록 했습니다.

IBM은 360/370일 전에 그들의 사양에서 예측 불가능이라는 단어를 사용했습니다.그것이 더 좋은 말입니다.그것은 결과를 더 무작위적이고 더 위험하게 만듭니다."예측할 수 없는" 행동의 범위 내에는 "멈추고 불이 붙는다"와 같은 문제가 있는 결과가 있습니다.

하지만 여기에 문제가 있습니다."랜덤"은 시스템이 문제에 직면할 때마다 다른 것을 수행할 수 있음을 의미하기 때문에 이러한 종류의 예측 불가능한 행동을 설명하는 나쁜 방법입니다.만약 그것이 매번 다른 것을 한다면, 여러분은 시험에서 문제를 잡을 가능성이 있습니다."정의되지 않은"/"예측 불가능한" 행동의 세계에서 시스템은 매번 동일한 작업을 수행합니다. 그렇지 않을 때까지요.그리고, 여러분은 여러분이 여러분의 물건들을 시험하는 것을 끝냈다고 생각하는 몇 년 후가 되지 않을 것이라는 것을 알고 있습니다.

따라서 사양서에 정의되지 않은 것이 있다고 표시될 때는 그런 짓을 하지 마십시오.머피의 친구가 아니라면요괜찮니?

"정의되지 않은 행동"은 "모든 것이 일어날 수 있다"를 의미합니다.일반적으로 "아무 일도 일어나지 않음"과 "코드 충돌"이 있습니다."무엇이든"의 다른 일반적인 값은 "최적화를 설정하면 나쁜 일이 발생한다"거나 "개발 중인 코드를 실행하지 않고 고객이 실행하고 있을 때 나쁜 일이 발생한다"는 것이고, 다른 값은 "당신의 코드가 예상치 못한 일을 수행한다"와 "당신의 코드가 수행해서는 안 되는 일을 수행한다"입니다.

그래서 만약 당신이 "C가 포인터가 사용되지 않고 경계 밖에 있는지 확인하는 것은 비논리적으로 들린다"고 말한다면, 당신은 매우, 매우, 매우 위험한 영역에 있는 것입니다.다음 코드를 사용합니다.

int a = 0;
int b [2] = { 1, 2 };
int* p = &a; p - 1;
printf ("%d\n", *p);

컴파일러는 정의되지 않은 동작이 없다고 가정할 수 있습니다. p - 1이 평가되었습니다.컴파일러는 p = &a [1], p = &b [1] 또는 p = &b [2] 중 하나로 결론짓습니다. 다른 모든 경우에 p를 평가할 때 또는 p-1을 평가할 때 정의되지 않은 동작이 있기 때문입니다.그런 다음 컴파일러는 *p가 정의되지 않은 동작이 아니라고 가정하므로 p = &b [1]로 결론을 내리고 값 2를 출력합니다.당신은 그것을 예상하지 못했죠, 그렇죠?

그것은 합법적이고, 실제로 일어납니다.그래서 교훈은 정의되지 않은 행동을 유발하지 말라는 것입니다.

일부 플랫폼은 포인터를 정수로 처리하고 포인터 산술을 정수 산술과 동일한 방식으로 처리하지만 개체의 크기에 따라 특정 값을 확장하거나 축소합니다.이러한 플랫폼에서, 이것은 포인터의 대상 유형의 크기의 배수가 아닌 차이를 가진 포인터의 뺄셈을 제외한 모든 포인터 산술 연산의 "자연스러운" 결과를 효과적으로 정의합니다.

다른 플랫폼은 다른 방식으로 포인터를 나타낼 수 있으며, 특정 포인터 조합을 추가하거나 뺄 경우 예측할 수 없는 결과가 발생할 수 있습니다.

C 표준의 저자들은 두 종류의 플랫폼 모두에 편애하는 것을 원하지 않았기 때문에 일부 플랫폼에서 문제를 일으킬 수 있는 방식으로 포인터가 조작될 경우 발생할 수 있는 일에 대한 요구 사항을 부과하지 않습니다.C 표준 이전과 그 이후 몇 년 동안 프로그래머들은 포인터 산술을 스케일 정수 산술처럼 다루는 플랫폼을 위한 범용 구현체들이 포인터 산술을 똑같이 다룰 것이라고 합리적으로 예상할 수 있었습니다.그러나 포인터 산술을 다르게 처리한 플랫폼의 구현은 그 자체로 다르게 처리할 가능성이 높습니다.

그러나 지난 10여 년 동안 컴파일러 작가들은 "최적화"를 추구하여 최소한의 경악의 원리를 창밖으로 던지기로 결정했습니다.프로그래머가 특정 포인터 연산의 효과가 플랫폼의 자연스러운 포인터 표현에 어떤 영향을 미치는지 알 수 있는 경우에도 컴파일러가 자연스러운 포인터 표현이 동작하는 방식으로 동작하는 코드를 생성한다는 보장은 없습니다.표준에서 동작이 정의되지 않았다고 말하는 것은 컴파일러가 기본 환경의 문서 동작과 일치하는 방식으로 단순히 동작하는 구현체에 필요한 것보다 더 느리고 덜렁거리는 코드를 작성하도록 강제하는 "최적화"를 부과하도록 하는 것으로 해석됩니다.C89의 저자들이 일반적이라고 명시적으로 언급한 세 가지 치료법).

따라서 이상한 "최적화"가 활성화되지 않은 컴파일러를 사용하고 있다는 것을 모르는 한 포인터 계산 시퀀스의 중간 단계가 정의되지 않은 동작을 호출한다는 사실은 그것에 대해 추론하는 것을 전혀 불가능하게 만듭니다.특정 플랫폼에 대한 품질 구현이 특정 방식으로 동작해야 한다는 것을 아무리 강력하게 상식적으로 암시하더라도 말입니다.

정의되지 않은 행동과 관련된 질문의 부분은 매우 명확합니다. 대답은 "글쎄요, 네, 확실히 정의되지 않은 행동입니다."입니다.

저는 "Do C check..."라는 문구를 해석할 것입니다.다음 두 가지로 표시됩니다.

  1. C 컴파일러가 확인...?
  2. 컴파일된 프로그램에서 확인할 수 있습니까?

(C 자체는 언어 사양이며, 어떤 것도 확인하거나 수행하지 않습니다.

첫 번째 질문에 대한 대답은: 네, 하지만 신뢰할 수 없고, 당신이 원하는 방식으로는 아닙니다.현대의 컴파일러는 매우 영리하며 때로는 당신이 원하는 것보다 더 똑똑합니다.컴파일러는 경우에 따라 잘못된 포인터 사용을 진단할 수 있습니다.정의는 정의되지 않은 동작을 호출하기 때문에 언어는 더 이상 컴파일러가 특정 작업을 수행할 필요가 없으므로 컴파일러는 종종 예측할 수 없는 방식으로 최적화됩니다.이로 인해 코드가 원래 의도한 것과 크게 다를 수 있습니다.전체 스코프 또는 전체 기능이 데드스트립된 경우에도 놀라지 마십시오.이는 정의되지 않은 동작과 관련하여 바람직하지 않은 많은 "깜짝 최적화"에 해당합니다.
필수 판독값:정의되지 않은 행동에 대해 모든 C 프로그래머가 알아야 할 것.

두 번째 질문에 대한 대답은 다음과 같습니다. 경계 검사를 지원하는 컴파일러를 사용하고 런타임 경계 검사를 사용하도록 설정하여 컴파일하는 경우를 제외하고는 매우 사소한 런타임 오버헤드를 의미합니다.
실제로, 이것은 프로그램이 정의되지 않은 동작을 최적화하는 컴파일러에서 "생존"했다면, 그것은 예측할 수 없는 결과로, 대개 가비지 값이 읽히거나 프로그램이 분할 오류를 발생시키는 등, 당신이 지시한 대로만 할 것이라는 것을 의미합니다.

하지만 정의되지 않은 행동은 무엇입니까?그것은 아무도 무슨 일이 일어날지 말하고 싶어하지 않는다는 것을 의미합니다.

저는 수년 전의 오래된 메인프레임 개입니다. 저는 IBM의 같은 말을 좋아합니다. 결과는 예측할 수 없습니다.

BTW: 저는 배열 경계를 확인하지 않는 이 좋습니다.예를 들어 문자열에 포인터가 있는데 바이트가 가리키기 직전에 무엇이 있는지 확인하려면 다음을 사용할 수 있습니다.

pointer[-1]

그것을 보기 위해서.

언급URL : https://stackoverflow.com/questions/41667731/does-c-check-if-a-pointer-is-out-of-bound-without-the-pointer-being-dereferenced

반응형