C 라이브러리의 함수는 항상 문자열 길이를 예상해야합니까?


15

현재 C로 작성된 라이브러리에서 작업하고 있습니다.이 라이브러리의 많은 함수는 인수 로 char*또는 문자열로 문자열을 기대합니다 const char*. size_tnull 종료가 필요하지 않도록 항상 문자열의 길이를 기대하는 함수로 시작했습니다 . 그러나 테스트를 작성할 때 strlen()다음과 같이 자주 사용되었습니다 .

const char* string = "Ugh, strlen is tedious";
libFunction(string, strlen(string));

사용자가 올바르게 종료 된 문자열을 전달하도록하면 덜 안전하지만 더 간결하고 읽기 쉬운 코드가됩니다.

libFunction("I hope there's a null-terminator there!");

그래서, 현명한 연습은 무엇입니까? API 사용을 더 복잡하게 만들지 만 사용자가 입력을 생각하거나 null로 끝나는 문자열에 대한 요구 사항을 문서화하고 호출자를 신뢰하도록 강요합니까?

답변:


4

가장 확실하고 절대적으로 길이를 가지고 다니십시오 . 표준 C 라이브러리는 이런 식으로 악의적으로 파괴되어 버퍼 오버플로를 처리하는 데 어려움을 겪지 않았습니다. 이 접근법은 현대의 컴파일러가 이런 종류의 표준 라이브러리 함수를 사용할 때 실제로 경고하고, 울리고, 불평 할 정도로 많은 증오와 고뇌의 초점입니다.

인터뷰에서이 질문을 접한 적이 있고 기술 면접관이 몇 년 동안 경험을 쌓은 것처럼 보이면 순수 열광자가 직업을 착륙시킬 수 있습니다. C 문자열 종결자를 찾는 API를 구현하는 사람 을 촬영하는 선례 .

그것의 감정을 제쳐두고, 문자열의 끝에서 NULL을 읽거나 조작하는 데 잘못 될 수있는 많은 것들이 있습니다-게다가 그것은 심층 방어와 같은 현대적인 디자인 개념을 직접 위반하는 것입니다 (보안에 반드시 적용되는 것이 아니라 API 디자인에 적용됨). 길이가 많은 C API의 예-예. Windows API.

실제로,이 문제는 90 년대 언젠가 해결되었는데, 오늘날 떠오르는 합의는 현을 만져서는 안된다는 것 입니다.

나중에 편집 : 당신은 같은 고전 물건을 볼 때까지 나는 OK가 아래로하고 위의 모든 사람을 신뢰하는 것은 좋은 것을 추가 * 기능을 라이브러리 STR을 사용합니다 있도록이 상당히 라이브 논쟁이다 output = malloc(strlen(input)); strcpy(output, input);또는 while(*src) { *dest=transform(*src); dest++; src++; }. 나는 배경에서 모차르트의 Lacrimosa를 거의들을 수있다.


1
호출자가 문자열 길이를 제공 해야하는 Windows API의 예를 이해하지 못합니다. 예를 들어, 일반적인 Win32 API 함수 CreateFileLPTCSTR lpFileName매개 변수를 입력으로 사용합니다. 호출자로부터 문자열의 길이가 예상되지 않습니다. 실제로 NUL로 끝나는 문자열의 사용은 파일 이름이 NUL로 끝나야 한다고 언급 하지도 않습니다 (물론 물론이어야합니다).
Greg Hewgill

1
실제로 Win32에서 LPSTR형식 은 문자열 NUL로 종료 될 수 있으며 그렇지 않은 경우 관련 사양에 표시됩니다. 따라서 달리 명시되지 않는 한 Win32의 이러한 문자열은 NUL로 종료 될 것으로 예상됩니다.
Greg Hewgill

좋은 지적, 나는 정확하지 않았다. CreateFile과 그 묶음은 Windows NT 3.1 (90 년대 초) 이후로 존재합니다. 현재 API (즉, XP SP2에 Strsafe.h가 도입 된 이후-Microsoft의 공개 사과와 함께)는 NULL로 끝나는 모든 항목을 명시 적으로 더 이상 사용하지 않습니다. Microsoft가 NULL로 끝나는 문자열을 사용하는 것에 대해 정말로 유감스럽게 생각한 것은 VB, COM 및 이전 WINAPI를 동일한 보트에 가져 오기 위해 OLE 2.0 사양에서 BSTR을 도입해야 할 때 실제로 훨씬 이전입니다.
vski

1
도에서 StringCbCat, 예를 들면, 오직 목적지 말이 최대 버퍼를 갖는다. 소스는 여전히 일반 NUL 종료 C 문자열입니다. 입력 매개 변수와 출력 매개 변수 의 차이점을 명확히하여 응답을 향상시킬 수 있습니다 . 출력 매개 변수는 항상 최대 버퍼 길이를 가져야합니다. 입력 매개 변수는 일반적으로 NUL로 종료됩니다 (예외는 있지만 경험상 드물습니다).
Greg Hewgill 2016 년

1
예. 플랫폼 수준의 JVM / Dalvik 및 .NET CLR과 기타 여러 언어에서 문자열을 변경할 수 없습니다. 나는 지금까지 가서 a) 레거시 (문자열의 일부만 변경하여 실제로 많은 것을 얻지 못함) 및 b 때문에 네이티브 세계가 아직 (C ++ 11 표준) 그렇게 할 수는 없다고 추측합니다. ) 실제로이 작업을 수행하려면 GC와 문자열 테이블이 필요합니다 .C ++ 11의 범위가 지정된 할당자는 잘릴 수 없습니다.
vski 2016 년

16

C에서 관용구는 문자열이 NUL로 끝나는 것이므로 일반적인 관례를 따르는 것이 합리적입니다. 실제로 라이브러리 사용자가 NUL로 끝나지 않은 문자열을 가질 가능성은 상대적으로 적습니다 (인쇄에 추가 작업이 필요하기 때문에) printf를 사용하고 다른 상황에서 사용). 다른 종류의 줄을 사용하는 것은 부자연스럽고 아마도 비교적 드 rare니다.

또한 상황에 따라 테스트가 나에게 조금 이상하게 보입니다. (스트 렌을 사용하여) 올바르게 작동하기 때문에 먼저 NUL 종료 문자열을 가정하고 있습니다. 라이브러리에서 작동하도록하려면 NUL이 아닌 문자열의 경우를 테스트해야합니다.


-1, 죄송합니다.이 방법은 잘못 권장됩니다.
vski

옛날에는 이것이 항상 사실이 아니 었습니다. NULL로 끝나지 않은 고정 길이 필드에 문자열 데이터를 넣는 이진 프로토콜을 많이 사용했습니다. 그러한 경우에는 시간이 오래 걸리는 기능을 사용하는 것이 매우 중요했습니다. 그래도 10 년 동안 C를하지 않았습니다.
로봇 고트

4
@ vski, 대상 함수를 호출하기 전에 사용자가 'strlen'을 강제로 호출하여 버퍼 오버플로 문제를 피하려면 어떻게해야합니까? 적어도 목표 함수 내에서 길이를 직접 확인하면 어떤 길이의 감각이 사용되는지 (터미널 널 포함 여부) 확신 할 수 있습니다.
찰스 E. 그랜트

@Charles E. Grant : Strsafe.h의 StringCbCat 및 StringCbCatN에 대한 위의 주석을 참조하십시오. char *이 있고 길이가없는 경우 실제로 str * 함수를 사용하는 것 외에는 실제 선택의 여지가 없지만 요점은 길이를 둘러싼 것이므로 str *와 strn * 사이의 옵션이됩니다. 후자가 선호되는 기능.
vski 2016 년

2
@vski 문자열 길이 를 전달할 필요가 없습니다 . 이 입니다 전세계 거의 통과 할 필요 버퍼 의 길이. 모든 버퍼가 문자열 인 것은 아니며 모든 문자열이 버퍼 인 것은 아닙니다.
jamesdlin

10

"안전성"주장은 실제로 유지되지 않습니다. 문서화 된 내용 (및 평범한 C의 표준)이있을 때 사용자가 null로 끝나는 문자열을 넘겨 줄 것을 믿지 않으면 실제로 그들이 제공 한 길이를 신뢰할 수 없습니다. 아마 strlen그들이 편리하지 않으면 당신이하고있는 것처럼 사용하여 얻을 수 있으며 , "문자열"이 처음에 문자열이 아니면 실패 할 것입니다).

길이가 필요한 유효한 이유가 있습니다. 함수를 서브 스트링에서 사용하려면 사용자가 널 바이트를 얻기 위해 마법을 앞뒤로 복사하는 것보다 길이를 전달하는 것이 훨씬 쉽고 효율적입니다. 올바른 장소에서 (그리고 도중에 오류가 발생할 위험이 있습니다).
널 바이트가 종료가 아닌 인코딩을 처리 할 수 ​​있거나 널 (임의로)이 포함 된 문자열을 처리 할 수 ​​있으면 일부 상황에서 유용 할 수 있습니다 (함수의 기능에 따라 다름).
널이 아닌 종료 데이터 (고정 길이 배열)도 처리 할 수 ​​있습니다.
간단히 말해서 : 라이브러리에서 수행중인 작업과 사용자가 처리 할 것으로 예상되는 데이터 유형에 따라 다릅니다.

이에 대한 성능 측면도있을 수 있습니다. 함수가 문자열의 길이를 미리 알아야하고 사용자가 최소한 일반적으로 해당 정보를 알고 있기 때문에 정보를 계산하지 않고 전달하면 몇 번의주기를 줄일 수 있습니다.

그러나 라이브러리에 일반 일반 ASCII 텍스트 문자열이 필요하고 성능에 제약이없고 사용자가 라이브러리와 상호 작용하는 방식을 잘 이해하고 있다면 길이 매개 변수를 추가하는 것은 좋은 생각처럼 들리지 않습니다. 문자열이 올바르게 종료되지 않으면 길이 매개 변수가 가짜 일 가능성이 있습니다. 나는 당신이 그것으로 많은 것을 얻을 것이라고 생각하지 않습니다.


이 접근 방식에 크게 동의하지 않습니다. 발신자, 특히 라이브러리 API 뒤에있는 발신자를 신뢰하지 마십시오. 발신자가 제공 한 내용에 의문을 제기하고 정상적으로 실패하도록 최선을 다하십시오. NULL로 끝나는 문자열로 작업하는 것이 "발신자와 느슨하고 엄격하게"라는 의미는 아닙니다.
vski

2
나는 주로 당신의 입장에 동의 하지만, 당신은 그 길이 논쟁에 많은 신뢰를하는 것 같습니다 -왜 널 터미네이터보다 신뢰할만한 이유가 없습니다. 내 입장은 그것이 도서관이하는 일에 달려 있다는 것입니다.
Mat

길이가 값에 의해 전달되는 것보다 문자열에서 NULL 종결자가 잘못 될 수있는 것이 훨씬 더 많습니다. C에서 길이를 신뢰할 수있는 유일한 이유는 길이가 불합리하고 비실용적이기 때문입니다. 버퍼 길이를 운반하는 것은 좋은 대답이 아니며 대안을 고려하는 것이 가장 좋습니다. 문자열 (및 일반적으로 버퍼)이 RAD 언어로 깔끔하게 압축되고 캡슐화되는 이유 중 하나입니다.
vski 2016 년

2

아니요. 문자열은 항상 정의에 의해 null로 끝나고 문자열 길이는 중복됩니다.

널이 아닌 문자 데이터는 "문자열"이라고해서는 안됩니다. 그것을 처리하고 (길이를 던지는) 일반적 으로 API의 일부가 아닌 라이브러리 내에 캡슐화 되어야 합니다. 단일 strlen () 호출을 피하기 위해 길이를 매개 변수로 요구하는 것은 조기 최적화 일 가능성이 높습니다.

API 함수의 호출자를 신뢰하는 것은 안전하지 않습니다 . 문서화 된 전제 조건이 충족되지 않으면 정의되지 않은 동작은 완벽하게 정상입니다.

물론 잘 설계된 API에는 함정이 없어야하며 올바르게 사용하기가 쉬워야합니다. 그리고 이것은 중복을 피하고 언어 관습에 따라 가능한 한 간단하고 간단해야 함을 의미합니다.


완벽하게 괜찮을뿐만 아니라 메모리에 안전한 단일 스레드 언어로 이동하지 않는 한 실제로 피할 수 없습니다. 몇 가지 더 필요한 제한 사항을 떨어 뜨릴 수 있습니다.
중복 제거기

1

항상 길이를 유지해야합니다. 예를 들어, 사용자는 NULL을 포함 할 수 있습니다. 두 번째로, strlenO (N)이며 모든 byby by by cache를 터치해야합니다. 셋째, 서브셋을 쉽게 전달할 수 있습니다. 예를 들어 서브셋은 실제 길이보다 줄어 듭니다.


4
라이브러리 함수가 문자열에 포함 된 NULL을 처리하는지 여부는 매우 잘 문서화되어야합니다. 대부분의 C 라이브러리 함수는 NULL 또는 길이 중 먼저 시작합니다. (그리고 길이를하지 않는 사람들은 사용하지 않습니다, 유능하게 작성된 경우 strlen루프 테스트에서.)
고트에게 로봇

1

문자열 을 전달하는 것과 버퍼를 전달 하는 것을 구별해야합니다 .

C에서 문자열은 전통적으로 NUL로 종료됩니다. 이것을 기대하는 것은 전적으로 합리적입니다. 따라서 일반적으로 문자열 길이를 전달할 필요가 없습니다. strlen필요한 경우 계산할 수 있습니다 .

전세계 거의 통과 할 때 버퍼 에 기록되고, 특히 하나를, 당신은 절대적으로 버퍼 크기에 따라 전달해야합니다. 대상 버퍼의 경우이를 통해 수신자는 버퍼 오버플로를 방지 할 수 있습니다. 입력 버퍼의 경우, 특히 입력 버퍼에 신뢰할 수없는 소스에서 시작된 임의의 데이터가 포함 된 경우 수신자는 끝을 지나서 읽지 않도록 할 수 있습니다.

문자열과 버퍼가 모두있을 수 있고 char*많은 문자열 함수가 대상 버퍼에 기록하여 새 문자열을 생성 하기 때문에 약간의 혼동이있을 수 있습니다 . 어떤 사람들은 문자열 함수가 문자열 길이를 가져야한다고 결론을 내립니다. 그러나 이것은 부정확 한 결론입니다. 버퍼에 크기를 포함시키는 방법 (문자열, 정수 배열, 구조 등에 버퍼를 사용하는지 여부)은보다 유용하고 일반적인 만트라입니다.

(신뢰할 수없는 소스 (예 : 네트워크 소켓)에서 문자열을 읽는 경우 입력이 NUL로 종료되지 않을 수 있으므로 길이를 제공하는 것이 중요 하지만 입력을 문자열로 간주 해서는 안됩니다 . 문자열을 포함 할 있는 임의의 데이터 버퍼로 처리해야 합니다 (그러나 실제로 유효성을 검사 할 때까지 알지 못합니다). 그래서 버퍼 크기와 관련하여 문자열에 필요하지 않다는 원칙을 따릅니다.


이것이 바로 질문과 다른 답변이 놓친 것입니다.
Blrfl

0

함수가 주로 문자열 리터럴과 함께 사용되는 경우 일부 매크로를 정의하여 명시적인 길이를 다루는 고통을 최소화 할 수 있습니다. 예를 들어, API 함수가 주어진 경우 :

void use_string(char *string, int length);

매크로를 정의 할 수 있습니다.

#define use_strlit(x) use_string(x, sizeof ("" x "")-1)

다음과 같이 호출하십시오.

void test(void)
{
  use_strlit("Hello");
}

컴파일되지만 실제로는 작동하지 않는 매크로를 전달하는 "크리에이티브"항목을 만들 수 있지만 """sizeof"평가에서 문자열의 양쪽에서 사용하면 실수로 문자를 사용하려는 시도를 잡아야합니다. 분해 된 문자열 리터럴 이외의 포인터 [이없는 경우 ""문자 포인터를 전달하려는 시도는 길이를 포인터 크기에서 1을 뺀 값으로 잘못 제공합니다.

C99의 대안은 "포인터 및 길이"구조 유형을 정의하고 문자열 리터럴을 해당 구조 유형의 복합 리터럴로 변환하는 매크로를 정의하는 것입니다. 예를 들면 다음과 같습니다.

struct lstring { char const *ptr; int length; };
#define as_lstring(x) \
  (( struct lstring const) {x, sizeof("" x "")-1})

그러한 접근 방식을 사용하는 경우 주소를 전달하지 않고 값으로 이러한 구조를 전달해야합니다. 그렇지 않으면 :

struct lstring *p;
if (foo)
{
  p = &as_lstring("Hello");
}
else
{
  p = &as_lstring("Goodbye!");
}
use_lstring(p);

복합 리터럴의 수명이 해당 명령문의 끝에서 끝나기 때문에 실패 할 수 있습니다.

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