[Reference] : 위 글은 다음 내용을 제가 공부한 후, 인용∙참고∙정리하여 만들어진 게시글입니다.
- 서론
- 타입 에러
- Q&A
- 마치며
자료형은 변수의 크기를 정의하고, 용도를 암시합니다.
자료형이 담고 있는 정보는 컴파일러에도 전달됩니다.
컴파일러는 변수의 자료형을 참고하여 변수에 관한 코드를 생성합니다.
그리고 각 변수에 대한 연산은 그 메모리 공간을 대상으로 이뤄집니다.
한 번 정의된 변수의 자료형은 바꿀 수 없습니다.
즉, 변수에 할당된 메모리의 크기는 확장되거나 줄어들지 않습니다.
이에 따라, 1바이트 크기의 변수에 1을 더하다가 그 값이 0xff
가 되면 0x100
이 아닌, 0x00
이 됩니다.
이러한 현상을 데이터가 넘쳐서 유실됐다고 하여 overflow라고 합니다.
마찬가지로, 변수의 크기보다 큰 값을 대입하려고 할 때도 데이터가 유실될 수 있습니다.
예를 들어 4바이트 크기의 변수에 0x0123456789abcdef
을 대입하려 하면, 하위 4바이트인 0x89abcdef
만 저장되고 나머지 값은 모두 버려집니다.
C언어에는 여러 자료형이 있습니다.
각각의 자료형은 저장할 수 있는 데이터 크기가 다르며, 일반적으로 저장하는 값의 용도가 정해져 있습니다.
주의해야 할 점은, 같은 자료형이라도 운영체제에 따라 크기가 달라질 수 있다는 것입니다.
예를 들어 long
은 32비트 운영체제에서는 4바이트의 크기를 갖기만, 64비트에서는 8바이트의 크기를 갖습니다.
변수의 자료형을 선언할 때는 변수를 활용하는 동안 담게 될 값의 크기, 용도, 부호 여부를 고려해야합니다.
Type Error는 이러한 고려 없이 부적절한 자료형을 사용했을 때 발생합니다.
// Name: out_of_range.c
// Compile: gcc -o out_of_range out_of_range.c
#include <stdio.h>
unsigned long long factorial(unsigned int n) {
unsigned long long res = 1;
for (int i = 1; i <= n; i++) {
res *= i;
}
return res;
}
int main() {
unsigned int n;
unsigned int res;
printf("Input integer n: ");
scanf("%d", &n);
if (n >= 50) {
fprintf(stderr, "Input is too large");
return -1;
}
res = factorial(n);
printf("Factorial of N: %u\n", res);
}
$ ./out_of_range
Input integer n: 17
Factorial of N: 4006445056
$ ./out_of_range
Input integer n: 18
Factorial of N: 3396534272
실행 결과를 보게 되면 18
에서 값이 갑자기 작아지는 것을 알 수 있습니다.
코드 상에서, 양수만을 곱했는데 값이 작아진 이유는 res
에 저장될 수 있는 범위보다 훨씬 큰 값을 저장하려 했기 때문입니다.
18!=0x16beecca730000입니다. 이를 4바이트 크기의 res
에 대입하려 하면, 상위 4바이트는 버려지고, 하위 4바이트인 0xca730000
만 옮겨집니다. 이는 출력된 값과 정확히 일치합니다.
이처럼, 변수에 어떤 값을 대입할 때, 그 값이 변수에 저장될 수 있는 범위를 벗어나면, 저장할 수 있는 만큼만 저장하고 나머지는 모두 유실됩니다.
// Name: oor_signflip.c
// Compile: gcc -o oor_signflip oor_signflip.c
#include <stdio.h>
unsigned long long factorial(unsigned int n) {
unsigned long long res = 1;
for (int i = 1; i <= n; i++) {
res *= i;
}
return res;
}
int main() {
int n;
unsigned int res;
printf("Input integer n: ");
scanf("%d", &n);
if (n >= 50) {
fprintf(stderr, "Input is too large");
return -1;
}
res = factorial(n);
printf("Factorial of N: %u\n", res);
}
$ ./oor_signflip
Input integer n: -1
위의 코드는 앞의 예제에서 main
함수의 변수 n
의 자료형이 int
로 바뀌었습니다.
이는 입력값으로 음수를 입력하면 main
함수의 if문을 우회할 수 있음을 의미합니다.
코드를 실행하고 -1
을 입력하면 아무리 기다려도 결과가 나오지 않습니다.
int n
에 -1
을 저장하면, n
의 메모리 공간에 저장되는 값은 0xffffffff
입니다.
그런데 factorial
함수는 unsigned int n
을 인자로 받으므로, 이 값은 부호 없는 정수인 4294967295로 전달되고, 결국4294967295번 반복문을 실행하게 됩니다. 당연히, 시간이 오래 걸릴 뿐만 아니라 값이 너무 커져서 연산도 제대로 이뤄지지 않습니다.
이런 문제를 예방하려면 양수로만 쓰일 값에 반드시 unsigned
를 붙이는 습관을 들여야 합니다.
// Name: oor_bof.c
// Compile: gcc -o oor_bof oor_bof.c -m32
#include <stdio.h>
#define BUF_SIZE 32
int main() {
char buf[BUF_SIZE];
int size;
printf("Input length: ");
scanf("%d", &size);
if (size > BUF_SIZE) {
fprintf(stderr, "Buffer Overflow Detected");
return -1;
}
read(0, buf, size);
return 0;
}
위의 코드는, 잘못된 자료형의 사용이 스택 버퍼 오버플로우로 이어지는 코드입니다.
버퍼 플로우를 막기 위해 size
가 32보다 작은지 검사합니다.
그러나 size
가 int
형이므로 음수를 전달한다면 검사를 우회할 수 있습니다.
이 때, read
함수의 세 번째 인자는 부호가 없는 size_t
형이므로, 음수로 전달하면 매우 큰 수로 해석됩니다.
( ※ size_t
는 부호 없는 정수를 저장하며, 32비트 운영체제에서는 4바이트, 64비트에서는 8바이트의 크기를 가집니다.)
실제로 size
에 -1
을 입력하고 32바이트보다 큰 데이터를 입력하면 다음과 같이 스택 버퍼 오버플로우가 발생합니다.
$ ./oor_bof
Input length: -1
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
*** stack smashing detected ***: <unknown> terminated
Aborted (core dumped)
64비트에서 컴파일했을 때는 동작이 이뤄지지 않습니다.
64비트 환경에서 -1
은 0xffffffffffffffff
입니다. size_t
는 64 비트 환경에서 8바이트 크기의 부호 없는 정수를 나타내므로, size_t로 환산하면 -1
은 18446744073709551615
이 됩니다. read함수는 count
의 값으로 이렇게 큰 값이 들어오면 아무런 동작도 하지 않고 에러값을 반환합니다.
변수의 값이 연산 중에 자료형의 범위를 벗어나면, 갑자기 크기가 작아지거나 커지는 현상이 발생하는데,
이를 'Type Overflow / Underflow'라고 합니다.
만약, 정수 자료형을 대상으로 발생하면 Type
에 Integer
를 넣어서 'Integer Overflow / Underflow'라고 합니다.
// Name: integer_example.c
// Compile: gcc -o integer_example integer_example.c
#include <limits.h>
#include <stdio.h>
int main() {
unsigned int a = UINT_MAX + 1;
int b = INT_MAX + 1;
unsigned int c = 0 - 1;
int d = INT_MIN - 1;
printf("%u\n", a);
printf("%d\n", b);
printf("%u\n", c);
printf("%d\n", d);
return 0;
}
$ ./integer_example
0
-2147483648
4294967295
2147483647
오버플로우가 발생하면 자료형이 표현할 수 있는 최솟값이 되며, 언더플로우가 발생하면 최댓값이 됩니다.
// Name: integer_overflow.c
// Compile: gcc -o integer_overflow integer_overflow.c -m32
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
unsigned int size;
scanf("%u", &size);
char *buf = (char *)malloc(size + 1);
unsigned int read_size = read(0, buf, size);
buf[read_size] = 0;
return 0;
}
위의 코드는 intger overflow가 힙 버퍼 오버플로우로 이어지는 코드입니다.
사용자로부터 size
값을 입력받고, size + 1
의 크기의 버퍼를 할당합니다. 그리고 그 버퍼에 size
만큼 입력을 받습니다.
만약 사용자가 size
에 unsigned int
의 최댓값인 4294967295
을 입력하면, integer overflow로 인해, size + 1
은 0
이 됩니다.
이 값이 malloc
에 전달되면, malloc
은 최소 할당 크기인 32바이트만큼 청크를 할당해줍니다.
반면, read
함수는 size
값을 그대로 사용합니다.
따라서 32바이트 크기의 청크에 4294967295만큼 값을 쓸 수 있는, 힙 버퍼 오버플로우가 발생하게 됩니다.
-
-
[Reference] : 위 글은 다음 내용을 제가 공부한 후, 인용∙참고∙정리하여 만들어진 게시글입니다.