희소 행렬(sparse matrix)을 "행 포인터 표"로 표현한다.
희소 행렬(Sparse Matrix)은 대부분의 원소가 0인 행렬을 말한다.
예시 :
0 0 0 0 5
0 0 3 0 0
0 0 0 0 0
7 0 0 0 0
0 0 0 9 0
rows[i] 는 i 번째 행
배열을 가리키며, 실제 데이터가 있는 행만 malloc 해서 연결한다.
맨 처음 실행해보니 main에서 체크섬을 계산하는 부분에서 터지는 것을 확인했다.
그래서 bt로 어디서부터 문제가 시작되는지 확인해보니 row_sum() 쪽으로 들어가고 있었고, 실제로 값을 읽는 부분에서 문제가 발생하고 있었다.
row_sum()에서 rows를 직접 확인해봤다.

pwndbg> x/4gx 0x5555555592a0
0x5555555592a0: 0x00005555555593b0 0x0000000000000000
0x5555555592b0: 0x00005555555593d0 0xababababababab
rows는 int **이기 때문에 각각의 값은 각 행을 가리키는 포인터다.
그런데 값을 보니
0x55555555593b0
0x0
0x55555555593d0
0xababababababab
처럼 정상적인 주소처럼 보이는 값 사이에 0x0, 0xabab... 같은 이상한 값이 섞여 있었다.
특히 0xabababab...가 수상쩍어서 이 0xAB 패턴은 어디서 온 값 인지 알아보기 위해 rows를 조금 더 자세히 확인해봄.
먼저 실제로 문제가 발생하는 행이 어디인지 확인하기 위해 값을 직접 출력해봤다.

pwndbg> print rows[1]
$4 = (int *) 0x0
rows[1]이 실제로 NULL이어서 rows[1][j]를 읽으려고 할 때 결국 *(rows[1] + j) 를 수행하게 되는데, rows[1] 자체가 0x0이므로 유효한 주소를 통해 값을 읽을 수 없는 상태였음.
이제
그럼 왜
rows[1]이 NULL이지?
를 따라가보기로 함.
make_matrix()로 들어가서 rows가 어떻게 만들어지는지 확인해봄.
static int **make_matrix(void) {
int **rows = malloc(ROWS * sizeof(int *));
if (!rows) {
perror("malloc");
exit(1);
}
for (int i = 0; i < ROWS; i += 2) {
int *r = malloc(COLS * sizeof(int));
for (int j = 0; j < COLS; j++)
r[j] = i * COLS + j;
rows[i] = r;
}
return rows;
}
!!이상 포인트 발견!!
for (int i = 0; i < ROWS; i += 2)
i가 0, 2, 4, 6...으로 증가하기 때문에
rows[0] → 할당됨
rows[1] → 아무것도 하지 않음
rows[2] → 할당됨
rows[3] → 아무것도 하지 않음
...
이런 상태가 됨.
즉, rows 배열 자체는 32개의 포인터를 저장할 공간을 확보했지만 그 안의 모든 포인터에 값을 넣은 것은 아니었음.
malloc()은 메모리 공간만 확보할 뿐, 그 공간을 자동으로 0으로 초기화하지 않기 때문에 int **rows = malloc(ROWS * sizeof(int *)); 를 했다고 해서 rows[0] ~ rows[31]이 자동으로 NULL이 되는 것은 아님.
그런데 여기서 또 의문이 생겼다.
그렇다면 초기화하지 않은
rows[1]이 왜0x0이고, 다른 곳에는 왜0xabababab...가 있는 거지?
그래서 dirty_heap()을 확인했다.
static void dirty_heap(void) {
void *scratch = malloc(ROWS * sizeof(int *));
if (scratch) {
memset(scratch, 0xAB, ROWS * sizeof(int *));
free(scratch);
}
}
main()에서 dirty_heap()이 먼저 호출되고 있었다.
malloc()으로 메모리 확보
↓
그 공간을 0xAB로 채움
↓
free()로 반환
↓
이후 make_matrix()가 같은 크기의 메모리를 `malloc()`
↓
힙 할당기가 방금 반환된 공간을 다시 사용
하는 흐름이어서 새로 malloc()한 rows의 일부 영역에 이전에 남아 있던 0xAB 패턴이 보일 수 있었던 것임.
즉, 초기화하지 않은 메모리를 읽는다는 것은 반드시 0이 나온다는 뜻이 아니고, 그 공간에 이전에 남아 있던 값이 그대로 보일 수도 있음!!
여기서 지난 Challenge 07과도 비교해봤다.
지난번에는 split_lines() 안의 지역 배열 parts의 주소를 함수 밖으로 넘긴 뒤, 함수가 끝난 다음 warm_stack()이 그 공간을 다시 사용하면서 문제가 발생했다.
이번에는 조금 다르다.
즉 둘 다 메모리의 상태를 잘못 가정해서 발생하지만, 문제가 시작되는 지점은 다름.
이번에는 rows[1]에 값을 넣는 코드 자체가 없었던 것이 원인이엇삼.
원인을 찾았으니 make_matrix()의 반복문을 수정했다.
//기존:
for (int i = 0; i < ROWS; i += 2)
//수정:
for (int i = 0; i < ROWS; i += 1)
이제 모든 인덱스를 순회하면서 각각의 행을 할당하게 된다.
rows[0] → malloc
rows[1] → malloc
rows[2] → malloc
rows[3] → malloc
...
rows[31] → malloc
다시 실행해보니 정상적으로 합이 계산됐다.
그런데 여기서 끝내면 안 됨!
make_matrix()를 수정하면서 할당하는 행의 개수가 바뀌었기 때문임.....
main()의 free 부분을 확인해보니 기존에도 똑같이 i += 2로 되어 있었다.
for (int i = 0; i < ROWS; i += 2)
free(rows[i]);
이 상태에서는 make_matrix()가 모든 행을 할당해도 짝수 번째 행만 free()하게 된다.
즉,
malloc: 0 1 2 3 4 5 ... 31
free: 0 2 4 6 ... 30
이 되어 홀수 번째 행의 메모리가 해제되지 않는다.
따라서 free도 동일하게 수정했다.
for (int i = 0; i < ROWS; i += 1)
free(rows[i]);
할당하는 쪽의 범위와 해제하는 쪽의 범위가 서로 맞아야 한다는 것을 다시 확인했다.
malloc()은 메모리를 할당할 뿐 초기화하지 않는다.0으로 초기화된다는 보장이 없다.calloc()이나 명시적인 초기화가 필요하다.0xAB 같은 이전 데이터가 남아 있을 수도 있다.NULL일 것이다라고 가정하면 안 된다.rows[i]처럼 포인터 배열을 사용할 때는 배열 공간을 할당한 것과 각 원소에 유효한 값을 넣는 것은 별개의 작업이다.i += 2처럼 일부 원소만 처리하는 반복문을 발견하면, 나머지 원소는 정말 의도적으로 비워둔 것인지 확인해야 한다.malloc하는 범위free하는 범위