Putting my CR hat on:

1 char *combine(s, t)
2 char *s, *t;
3 {
4
5 int x, y;
6 char r[100];
7
8 strcpy(r, s);
9 y = strlen(r);
10 for (x = y; *t != '\0'; ++x)
11 r[x] = *t++;
12
13 r[x] = '\0';
14
15 return(r);
16
17 }

There are several critical memory errors here.

1) The function is returning the address of a local variable. This alone makes this function rubbish.

2) The pointers s and t are unknown length, we have no guarantees that concatenating them will fit in a 100 character array. Also, he probably should be using malloc to dynamically allocate the space.

3) Use of the function strcpy rather than strncpy. He should have measured the length of s first, and then use strncpy if s was longer than 99 characters (don't forget the null terminator!). Then, rather than using a for loop, call strncpy again to copy the rest into a safe buffer size (the for loop is rather silly). The reason for this is that strcpy will cheerfully start copying past the array 'boundaries', and in this case, since he's copying into a local variable on the stack, is setting himself up for a Remote Code Execution attack if this ever gets untrusted input.

So those are the critical errors. These tie directly into why Geoff argues that the author doesn't understand the stack.

So let's, for educational purposes, go into this. We're going to go a bit into the weeds here. Sorry about that. This would be easier with a whiteboard. :)

When you fire up a program, the programs machine instructions get copied into memory, let's pretend at the memory location 0x1000. Far away from that code, at the highest memory values (more complicated on modern virtual systems, but hey, let's go back in time here :) ), the computer keeps track of a location called the stack pointer.

I'm going to put forward 3 diagrams now. Please forgive any off by one errors.

(Diagram a)
Registers
A 0
B 0
C 0
SP 0xffff
PC 0x1000

Address Mnemonic DATA
PC0x1000 MOV 1, A 0x00 0x01 0x01 0x01
0x1004 MOV A, C 0x00 0x01 0x03 0x01
0x1008 PUSH 3 0x01 0x00 0x00 0x03
... ...
... ...
... ...
0xfffc XXXXXXXX 0x00 0x00 0x00 0x00 <-- Stack Pointer is here

The program starts at 0x1000, then after executing the first two move (MOV) instructions, the state of the world becomes as follows

(Diagram b)
Registers
A 1
B 0
C 1
SP 0xfffe
PC 0x1000

Address Mnemonic DATA
0x1000 MOV 1, A 0x00 0x01 0x01 0x01
0x1004 MOV A, C 0x00 0x01 0x03 0x01
PC0x1008 PUSH 3 0x01 0x00 0x00 0x03
... ...
... ...
... ... v------\
0xfffc XXXXXXXX 0x00 0x00 0x00 0x03 ^-- Stack Pointer is here

When you have code like

void function() {
int a = 5;
int b = 2;
return a;
}

void main() {
return function();
}

It'll get turned into something like (I've set a 'break point' at 0x2010)

(Diagram c)
Registers
A 5
B 0
C 0
SP 0xfff8
PC 0x2010

Address Mnemonic DATA
# Main starts here
# (Note, in C, there is actually code that gets executed before this)
PC0x1000 PUSH 0x1008 # We want to remember where to return to, so we push it to the stack.
0x1004 JMP 0x2000
0x1008 EXIT A # In this implementation of C, the A register will propagate return values
... ...
# Function 'function' is here
0x2000 PUSH 5 # Local variables go on the stack.
0x2004 PUSH 2
0x2008 MOV [SP+2], A # Locally, we refer to local variables by
# offsets to the stack pointer, so if this function
# were to call itself, the stack would keep growing down
# but these values would be good.
0x200c MOV SP+2, SP # Reset the stack before returning
PC0x2010 JMP #SP # Made up notation. Look at the value of the stack pointer, pop it, and jump to it.
# in x86, this is kinda what RET does.
... ...
... ... ...
... ... ...
0xfff8 XXXXXXXX 0x00 0x00 0x00 0x00SP
0xfffc XXXXXXXX 0x02 0x05 0x10 0x08

Okay! So with the above diagrams in mind, let's recap what goes on the stack. Local variables and return addresses. Each time a function gets called it moves the stack pointer down[1] (to lower memory addresses) to make room for local variables. So after you return from that function, and then call another function (or heck, the same one) that pointer you have that was supposed to be the concatenated string is now going to have it's values overwritten.

Furthermore, if the input strings are longer than expected, than they can overwrite values on the stack itself, including the return addres, causing your program to jump to some (if you're lucky) random location in memory.

Honestly, some of the best ways to get intuition for how the stack works, and the things that can go wrong, are CTFS at overthewire.org.

Also, https://microcorruption.com/

http://overthewire.org/wargames/bandit/

http://overthewire.org/wargames/leviathan/

[1] Sorry, 'down' means lower memory addresses, even though the displays of memory layouts always have lower memory addresses "up". :(