Reputation: 124
I am programming a terminal snake game in C. User input is handled using a pthread which reads the direction pointer. To account for the direction of the snake, a direction vector, allocated dynamically, is increased in size each time the snake eats a fruit. For that, I created an append() function which takes the vector pointer as a argument, the current size of the snake and the value to be appended (last direction).
The game runs normally until the current size is 5 and then it aborts with the "double free or corruption" error. I have absolutely no idea why that happens. Any hints or ideas will be appreciated.
Here is the full code:
// a simple snake game which uses pthread.h library to handle user input
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <termios.h> // for getch
#include <errno.h>
#include <unistd.h>
#include <pthread.h>
#define SIZE_X 20
#define SIZE_Y 20
#define TIME 200 // game update interval (milliseconds)
bool collision;
typedef struct position {
int x;
int y;
} position;
int *append(int *v, int size, int value) // append data to vector
{
size = size + 1;
puts("Reallocing memory ...");
printf("\nCurrent size: %d\n", size);
v = (int*)realloc(v, (size)*sizeof(int*));
puts("Memory reallocating sucessful!");
v[size-1] = value;
}
int msleep(long msec) // sleep for the requested number of milliseconds
{
struct timespec ts;
int res;
if (msec < 0)
{
errno = EINVAL;
return -1;
}
ts.tv_sec = msec / 1000;
ts.tv_nsec = (msec % 1000) * 1000000;
do {
res = nanosleep(&ts, &ts);
} while (res && errno == EINTR);
return res;
}
char getch(void) // get char
{
char buf = 0;
struct termios old = {0};
fflush(stdout);
if(tcgetattr(0, &old) < 0)
perror("tcsetattr()");
old.c_lflag &= ~ICANON;
old.c_lflag &= ~ECHO;
old.c_cc[VMIN] = 1;
old.c_cc[VTIME] = 0;
if(tcsetattr(0, TCSANOW, &old) < 0)
perror("tcsetattr ICANON");
if(read(0, &buf, 1) < 0)
perror("read()");
old.c_lflag |= ICANON;
old.c_lflag |= ECHO;
if(tcsetattr(0, TCSADRAIN, &old) < 0)
perror("tcsetattr ~ICANON");
printf("%c\n", buf);
return buf;
}
int randomInt(int b, int a) // generates random integer between b and a
{
return ((rand() % (b+1 - a)) + a);
}
void *readMove(void *data) // argument to p_thread to read user input
{
int *direction = ((int*) data); // pass function void pointer to direction pointer
pthread_detach(pthread_self());
while (collision) {
if (getch() == '\033') { // if the first value is esc
fputs("\033[A\033[2K",stdout);
rewind(stdout);
getch(); // skip the [
fputs("\033[A\033[2K",stdout);
rewind(stdout);
switch(getch()) { // the real value
case 'A':
// code for arrow up
fputs("\033[A\033[2K",stdout);
rewind(stdout);
*direction = 0;
break;
case 'B':
// code for arrow down
fputs("\033[A\033[2K",stdout);
rewind(stdout);
*direction = 2;
break;
case 'C':
// code for arrow right
fputs("\033[A\033[2K",stdout);
rewind(stdout);
*direction = 1;
break;
case 'D':
// code for arrow left
fputs("\033[A\033[2K",stdout);
rewind(stdout);
*direction = 3;
break;
}
}
}
pthread_exit(NULL);
}
void beginGame() // welcome screen
{
int i;
for (i = 0; i < 4; i++) {
printf("WELCOME TO C-SNAKE! - Initializing game in %d seconds ...\n", (3-i));
sleep(1);
fputs("\033[A\033[2K",stdout);
rewind(stdout);
}
}
void clearScreen() // clear game screen
{
int i;
for (i=0; i <= SIZE_Y; i++) {
fputs("\033[A\033[2K",stdout);
rewind(stdout);
}
}
void printGame(char game[SIZE_X][SIZE_Y]) // print current game
{
int i, j;
printf("\r");
for (i=0; i<SIZE_X; i++) {
printf("\n");
for (j=0; j<SIZE_Y; j++) {
printf("%c ", game[i][j]);
}
}
printf("\n");
}
void initializeGame(char game[SIZE_X][SIZE_Y], position *player_position, int *direction) // initializes variables
{
int i, j;
// initializes movement direction - 0: up, 1: right, 2: down, 3: left
*direction = 2;
// initializes player position
player_position->x = 1;
player_position->y = 1;
// initializes board
for (i=0; i<SIZE_X; i++) {
for (j=0; j<SIZE_Y; j++) {
if ((i == player_position->x) && (j == player_position->y)) {
game[i][j] = 'o';
}
else {
game[i][j] = '_';
}
}
}
}
bool checkCollision(position *player_position) // checks for collision (game over)
{
if (player_position->x == 0) {
return false;
}
else if (player_position->y == 0) {
return false;
}
else if (player_position->x == (SIZE_X-1)) {
return false;
}
else if (player_position->y == (SIZE_Y-1)) {
return false;
}
else {
return true;
}
}
void updateGame(char game[SIZE_X][SIZE_Y], position *player_position, int *direction, int it, int *length) // updates snake position
{
int i, j, x_inc=0, y_inc=0;
switch (direction[it])
{
case 0:
x_inc = -1;
break;
case 1:
y_inc = 1;
break;
case 2:
x_inc = 1;
break;
case 3:
y_inc = -1;
break;
}
player_position->x = player_position->x+x_inc;
player_position->y = player_position->y+y_inc;
for (i=0; i<SIZE_X; i++) {
for (j=0; j<SIZE_Y; j++) {
if ((i == player_position->x) && (j == player_position->y)) {
if (game[i][j] == '+') {
*length = *length+1;
append(direction, *length, direction[it]);
}
game[i][j] = 'o';
}
else if (game[i][j] != '+') {
game[i][j] = '_';
}
}
}
}
void generateFruit(char game[SIZE_X][SIZE_Y]) // generate random fruits
{
position fruit_position;
while (true) {
fruit_position.x = randomInt(SIZE_X-2, 1);
fruit_position.y = randomInt(SIZE_Y-2, 1);
if (game[fruit_position.x][fruit_position.y] == '_') {
game[fruit_position.x][fruit_position.y] = '+';
break;
}
}
}
int main(int argc, char *argv[])
{
char game[SIZE_X][SIZE_Y]; // game tiles
int i=0; // auxiliary variable
int length = 0; // initial length
int current_length;
int counter = 0; // time counter
float total_time; // total game time
int fruit_interval = 10; // time interval (seconds) to random fruit generation
position player_position; // player head position (x,y)
int *direction = (int*)malloc(1*sizeof(int*)); // moving direction
int rc;
pthread_t pthread; // thread to handle player input
srand(time(NULL)); // uses time to generate random seed
beginGame(); // intro screen
initializeGame(game, &player_position, direction); // initializes variables
collision = checkCollision(&player_position);
rc = pthread_create(&pthread, NULL, readMove, (void *)direction);
if (rc) {
printf("\nError - return code from pthread_create is %d\n", rc);
return EXIT_FAILURE;
}
while (collision) {
collision = checkCollision(&player_position);
printGame(game);
msleep(TIME);
clearScreen();
updateGame(game, &player_position, direction, i, &length);
counter++;
if ((counter % fruit_interval) == 0) {
generateFruit(game);
}
}
total_time = TIME*counter/1000;
printf("\n*** GAME OVER! ***\n\n");
printf("\nTotal time: %.1fs\n\n", total_time);
puts("Press any key to quit ...");
free(direction);
pthread_exit(NULL);
}
Upvotes: 1
Views: 654
Reputation: 213754
I have absolutely no idea why that happens.
This happens because you've corrupted heap in some way. Common ways: free
ing something twice, free
ing unallocated (e.g. pointing to stack) pointer, writing before or after the end of the block, etc.
These heap corruption errors are often hard to debug using code inspection or a debugger, because the error is usually detected much later in the execution, possibly in completely unrelated code.
Fortunately, there are specialized tools which make finding these bugs much easier. If your platform supports Address Sanitizer, you are in luck: simply rebuild your program with gcc -fsanitize=address -g ...
and it will point you right at the error. Valgrind is an earlier tool which can also help with the same class of bugs.
Here is output from running the game with -fsanitize=address
:
$ ./a.out
Reallocing memory ...
Current size: 2
Memory reallocating sucessful!
=================================================================
==1713589==ERROR: AddressSanitizer: heap-use-after-free on address 0x602000000010 at pc 0x563c5223c583 bp 0x7ffdacfb9a10 sp 0x7ffdacfb9a08
READ of size 4 at 0x602000000010 thread T0
#0 0x563c5223c582 in updateGame /tmp/t.c:204
#1 0x563c5223cd5f in main /tmp/t.c:284
#2 0x7f8cb9f64e49 in __libc_start_main ../csu/libc-start.c:314
#3 0x563c5223b2c9 in _start (/tmp/a.out+0x22c9)
0x602000000010 is located 0 bytes inside of 8-byte region [0x602000000010,0x602000000018)
freed by thread T0 here:
#0 0x7f8cba1d1b48 in __interceptor_realloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:164
#1 0x563c5223b3d8 in append /tmp/t.c:27
#2 0x563c5223c816 in updateGame /tmp/t.c:228
#3 0x563c5223cd5f in main /tmp/t.c:284
#4 0x7f8cb9f64e49 in __libc_start_main ../csu/libc-start.c:314
previously allocated by thread T0 here:
#0 0x7f8cba1d17cf in __interceptor_malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145
#1 0x563c5223cc3d in main /tmp/t.c:263
#2 0x7f8cb9f64e49 in __libc_start_main ../csu/libc-start.c:314
...
Heap use after free means: you've free
d some memory (e.g. realloc
frees the old memory block), but continue using it afterwards.
It's exceedingly likely that append
should return v;
, and the caller should be changed like so:
if (game[i][j] == '+') {
*length = *length+1;
- append(direction, *length, direction[it]);
+ direction = append(direction, *length, direction[it]);
}
game[i][j] = 'o';
However, even with above changes, other heap corruption bugs remain.
This is because (as dirck commented earlier), you have two instances of direction
-- one in the main thread, and one in the readMove
one. The variable needs to be updated in both, and the access to this variable should be synchronized.
Upvotes: 1