IT박스

부호없는 정수가 오류가 발생하기 쉬운 이유는 무엇입니까?

itboxs 2020. 12. 4. 08:01
반응형

부호없는 정수가 오류가 발생하기 쉬운 이유는 무엇입니까?


나는 이 비디오를 보고 있었다 . Bjarne Stroustrup서명되지 않은 int 는 오류가 발생하기 쉽고 버그로 이어진다 고 말합니다 . 따라서 정말 필요할 때만 사용해야합니다. 또한 Stack Overflow에 대한 질문 중 하나를 읽었지만 서명되지 않은 int 를 사용 하면 보안 버그가 발생할 수 있다는 것을 기억하지 못합니다 .

보안 버그는 어떻게 발생합니까? 누군가가 적절한 예를 들어 명확하게 설명 할 수 있습니까?


한 가지 가능한 측면은 언더 플로가 많은 수로 이어지기 때문에 부호없는 정수는 루프에서 다소 찾기 어려운 문제로 이어질 수 있다는 것입니다. 이 버그의 변형을 몇 번이나 만들 었는지 셀 수 없습니다 (부호없는 정수로도!)

for(size_t i = foo.size(); i >= 0; --i)
    ...

정의상 i >= 0항상 true입니다. (처음에이 문제를 일으키는 원인 i은 서명 된 경우 컴파일러가 size_tof 과 함께 오버플로 가능성에 대해 경고하기 때문 입니다 size().)

위험을 언급 한 다른 이유가 있습니다 – 여기에 서명되지 않은 유형이 사용되었습니다! , 그중 가장 강력한 것은 부호있는 형식과 부호없는 형식 간의 암시 적 형식 변환입니다.


한 가지 큰 요인은 루프 로직을 더 어렵게 만든다는 것입니다. 배열의 마지막 요소 (실제 세계에서 발생)를 제외한 모든 요소를 ​​반복한다고 상상해보십시오. 따라서 함수를 작성합니다.

void fun (const std::vector<int> &vec) {
    for (std::size_t i = 0; i < vec.size() - 1; ++i)
        do_something(vec[i]);
}

좋아 보이지 않습니까? 매우 높은 경고 수준으로 깔끔하게 컴파일됩니다! ( Live ) 그래서 이것을 코드에 넣으면 모든 테스트가 원활하게 실행되고 잊어 버립니다.

이제 나중에 누군가가 vector함수에 빈 패스를 전달합니다 . 이제 부호있는 정수를 사용하면 부호 비교 컴파일러 경고를 발견 하고 적절한 캐스트를 도입했으며 처음에 버그가있는 코드를 게시하지 않았을 것입니다.

그러나 부호없는 정수를 사용하는 구현에서는 래핑하고 루프 조건은 i < SIZE_T_MAX. 재해, UB 및 충돌 가능성이 가장 높습니다!

보안 버그로 이어지는 방법을 알고 싶습니다.

이것은 또한 보안 문제이며, 특히 버퍼 오버플로 입니다. 이를 악용 할 수있는 한 가지 방법 do_something은 공격자가 관찰 할 수있는 작업을 수행하는 것입니다. 그는 어떤 입력이으로 들어 갔는지 알아낼 수 있으며 do_something, 그렇게하면 공격자가 액세스 할 수 없어야하는 데이터가 메모리에서 유출됩니다. 이것은 Heartbleed 버그 와 유사한 시나리오 입니다. (그의 의견 에서 지적한 래칫 괴물에게 감사드립니다 .)


질문에 답하기 위해 비디오를 보지 않겠습니다.하지만 한 가지 문제는 부호있는 값과 부호없는 값을 혼합하면 발생할 수있는 혼란스러운 변환입니다. 예를 들면 :

#include <iostream>

int main() {
    unsigned n = 42;
    int i = -42;
    if (i < n) {
        std::cout << "All is well\n";
    } else {
        std::cout << "ARITHMETIC IS BROKEN!\n";
    }
}

승격 규칙은 비교 iunsigned위해 로 변환되어 큰 양수와 놀라운 결과를 제공합니다.


이 단지 기존의 답변의 변종으로 간주 될 수 있지만 : 참조 "인터페이스에 서명과 서명되지 않은 유형,"C ++ 보고서 1995 년 9 월 스콧 마이어스에 의해, 그것의 부호없는 형식을 피하기 위해 특히 중요 인터페이스 .

문제는 인터페이스의 클라이언트가 만들 수있는 (그리고 그들이 경우에 확신 오류를 감지하는 것이 불가능하게한다는 것입니다 그들을 만들, 그들은 그들을 만들 참조).

주어진 예는 다음과 같습니다.

template <class T>
  class Array {
  public:
      Array(unsigned int size);
  ...

이 클래스의 가능한 인스턴스화

int f(); // f and g are functions that return
int g(); // ints; what they do is unimportant
Array<double> a(f()-g()); // array size is f()-g()

값의 차이에 의해 반환 f()g()이유의 끔찍한 번호, 음수가 될 수 있습니다. Array클래스 의 생성자 는 암시 적으로로 변환되는 값으로이 차이를받습니다 unsigned. 따라서 Array클래스 의 구현 자로서 잘못 전달 된 값 -1과 매우 큰 배열 할당을 구별 할 수 없습니다 .


unsigned int의 가장 큰 문제는 unsigned int 0에서 1을 빼면 결과는 음수가 아니고 결과는 처음에 시작한 숫자보다 작지 않지만 결과는 가능한 가장 큰 unsigned int 값이라는 것입니다. .

unsigned int x = 0;
unsigned int y = x - 1;

if (y > x) printf ("What a surprise! \n");

And this is what makes unsigned int error prone. Of course unsigned int works exactly as it is designed to work. It's absolutely safe if you know what you are doing and make no mistakes. But most people make mistakes.

If you are using a good compiler, you turn on all the warnings that the compiler produces, and it will tell you when you do dangerous things that are likely to be mistakes.


The problem with unsigned integer types is that depending upon their size they may represent one of two different things:

  1. Unsigned types smaller than int (e.g. uint8) hold numbers in the range 0..2ⁿ-1, and calculations with them will behave according to the rules of integer arithmetic provided they don't exceed the range of the int type. Under present rules, if such a calculation exceeds the range of an int, a compiler is allowed to do anything it likes with the code, even going so far as to negate the laws of time and causality (some compilers will do precisely that!), and even if the result of the calculation would be assigned back to an unsigned type smaller than int.
  2. Unsigned types unsigned int and larger hold members of the abstract wrapping algebraic ring of integers congruent mod 2ⁿ; this effectively means that if a calculation goes outside the range 0..2ⁿ-1, the system will add or subtract whatever multiple of 2ⁿ would be required to get the value back in range.

Consequently, given uint32_t x=1, y=2; the expression x-y may have one of two meanings depending upon whether int is larger than 32 bits.

  1. If int is larger than 32 bits, the expression will subtract the number 2 from the number 1, yielding the number -1. Note that while a variable of type uint32_t can't hold the value -1 regardless of the size of int, and storing either -1 would cause such a variable to hold 0xFFFFFFFF, but unless or until the value is coerced to an unsigned type it will behave like the signed quantity -1.
  2. If int is 32 bits or smaller, the expression will yield a uint32_t value which, when added to the uint32_t value 2, will yield the uint32_t value 1 (i.e. the uint32_t value 0xFFFFFFFF).

IMHO, this problem could be solved cleanly if C and C++ were to define new unsigned types [e.g. unum32_t and uwrap32_t] such that a unum32_t would always behave as a number, regardless of the size of int (possibly requiring the right-hand operation of a subtraction or unary minus to be promoted to the next larger signed type if int is 32 bits or smaller), while a wrap32_t would always behave as a member of an algebraic ring (blocking promotions even if int were larger than 32 bits). In the absence of such types, however, it's often impossible to write code which is both portable and clean, since portable code will often require type coercions all over the place.


Numeric conversion rules in C and C++ are a byzantine mess. Using unsigned types exposes yourself to that mess to a much greater extent than using purely signed types.

Take for example the simple case of a comparison between two variables, one signed and the other unsigned.

  • If both operands are smaller than int then they will both be converted to int and the comparison will give numerically correct results.
  • If the unsigned operand is smaller than the signed operand then both will be converted to the type of the signed operand and the comparison will give numerically correct results.
  • If the unsigned operand is greater than or equal in size to the signed operand and also greater than or equal in size to int then both will be converted to the type of the unsigned operand. If the value of the signed operand is less than zero this will lead to numerically incorrect results.

To take another example consider multiplying two unsigned integers of the same size.

  • If the operand size is greater than or equal to the size of int then the multiplication will have defined wraparound semantics.
  • If the operand size is smaller than int but greater than or equal to half the size of int then there is the potential for undefined behaviour.
  • If the operand size is less than half the size of int then the multiplication will produce numerically correct results. Assigning this result back to a variable of the original unsigned type will produce defined wraparound semantics.

In addition to range/warp issue with unsigned types. Using mix of unsigned and signed integer types impact significant performance issue for processor. Less then floating point cast, but quite a lot to ignore that. Additionally compiler may place range check for the value and change the behavior of further checks.

참고URL : https://stackoverflow.com/questions/30395205/why-are-unsigned-integers-error-prone

반응형