I have a piece of code that uses setjmp/longjmp for control flow between two functions. I noticed some unexpected behavior where the loop counter seems to skip values. Here's a minimal example that demonstrates the issue:
#include <stdio.h>
#include <setjmp.h>
jmp_buf routineA, routineB;
void routine_a()
{
for (int i = 0; i < 10; ++i)
{
printf("A %d\n", i);
if (!setjmp(routineA)) {
if (i == 0)
return;
longjmp(routineB, 1);
}
}
}
void routine_b()
{
for (int i = 0; i < 10; ++i)
{
printf("B %d\n", i);
if (!setjmp(routineB))
longjmp(routineA, 1);
}
}
int main()
{
routine_a();
routine_b();
return 0;
}
The output of this code is:
A 0
B 0
A 1
B 2
A 3
B 4
A 5
B 6
A 7
B 8
A 9
I'm confused about why the numbers are skipping. For example, when i=1 in routine_a, it jumps to routine_b, but when control returns to routine_a, the next value printed is 3, skipping 2 entirely. I understand that longjmp breaks the normal control flow, but I don't understand:
I would appreciate any insights into what's happening under the hood.
I tried to ask AI(chatgpt claude), but still couldn't find the answer.
I have a piece of code that uses setjmp/longjmp for control flow between two functions. I noticed some unexpected behavior where the loop counter seems to skip values. Here's a minimal example that demonstrates the issue:
#include <stdio.h>
#include <setjmp.h>
jmp_buf routineA, routineB;
void routine_a()
{
for (int i = 0; i < 10; ++i)
{
printf("A %d\n", i);
if (!setjmp(routineA)) {
if (i == 0)
return;
longjmp(routineB, 1);
}
}
}
void routine_b()
{
for (int i = 0; i < 10; ++i)
{
printf("B %d\n", i);
if (!setjmp(routineB))
longjmp(routineA, 1);
}
}
int main()
{
routine_a();
routine_b();
return 0;
}
The output of this code is:
A 0
B 0
A 1
B 2
A 3
B 4
A 5
B 6
A 7
B 8
A 9
I'm confused about why the numbers are skipping. For example, when i=1 in routine_a, it jumps to routine_b, but when control returns to routine_a, the next value printed is 3, skipping 2 entirely. I understand that longjmp breaks the normal control flow, but I don't understand:
I would appreciate any insights into what's happening under the hood.
I tried to ask AI(chatgpt claude), but still couldn't find the answer.
Is this behavior defined by the C standard or implementation-dependent?
The behavior of your program is not defined by the C standard.
C 2024 7.13.3.1 says, of the invocation of longjmp
:
… The longjmp function restores the environment saved by the most recent invocation of the
setjmp
macro in the same invocation of the program with the correspondingjmp_buf
argument. If … the function containing the invocation of thesetjmp
macro has terminated execution in the interim,… the behavior is undefined.
All prior C standards have similar wording. So, when a function calls setjmp
and then returns, the behavior of later passing the associated jmp_buf
object to longjmp
is not defined by the C standard.
Why exactly are values being skipped?
Most commonly, setjmp
and longjmp
implementations rely on the stack to preserve program state. jmp_buf
is an object of fixed size, so it is impossible in general to store all of a function’s variables in it. Primarily, jmp_buf
holds processor registers and other processor state, including the value of the stack pointer. When longjmp
is invoked, restoring the value of the stack pointer, along with other processor state, makes the original stack frame active again if it is still present on the stack. The stack frame contains the values of the function’s variables, unless optimization put them in processor registers.
What happens to the loop counter when longjmp is called?
What is likely happening in your specific program is:
routine_a
is called. It uses stack space for i
and initializes it to 0. routine_a
returns.routine_b
is called. It uses the same stack space for its i
and initializes it to 0. routine_b
invokes longjmp
, which transfers control to routine_a
.routine_a
continues execution through the ++i
portion of its loop, which changes the value of i
(set to 0 in routine_b
) to 1. It prints “1”. Then routine_a
invokes longjmp
, which transfers control to routine_b
.routine_b
continues execution through its ++i
, which changes the value of i
(set to 1 in routine_a
) to 2. It prints “2”. Then routine_b
invokes longjmp
, which transfers control to routine_a
.routine_a
continues execution through its ++i
, which changes i
from 2 to 3. It prints “3”.i
.As soon as you leave either function, the local variables go out of scope and you can't jump in there again hoping to find the variables magically preserved.
To illustrate this with gcc 14.2 x86 -O3
:
#include <stdio.h>
#include <setjmp.h>
jmp_buf routineA, routineB;
int* last_i;
void routine_a()
{
int i;
for (i = 0; i < 10; ++i)
{
printf("A stored at address %p: %d\n", &i, i);
if (!setjmp(routineA)) {
if (i == 0) { last_i=&i; return; }
longjmp(routineB, 1);
}
}
last_i=&i;
}
void routine_b()
{
int i;
for (i = 0; i < 10; ++i)
{
printf("B stored at address %p: %d\n", &i, i);
if (!setjmp(routineB)) longjmp(routineA, 1);
}
last_i=&i;
}
int main()
{
routine_a();
printf("A::i is now dead. The memory location might contain %d still.\n", *last_i);
routine_b();
printf("B::i is now dead. The memory location might contain %d still.\n", *last_i);
return 0;
}
Output:
A stored at address 0x7ffc26a56dec: 0
A::i is now dead. The memory location might contain 0 still.
B stored at address 0x7ffc26a56dec: 0
A stored at address 0x7ffc26a56dec: 1
B stored at address 0x7ffc26a56dec: 2
A stored at address 0x7ffc26a56dec: 3
B stored at address 0x7ffc26a56dec: 4
A stored at address 0x7ffc26a56dec: 5
B stored at address 0x7ffc26a56dec: 6
A stored at address 0x7ffc26a56dec: 7
B stored at address 0x7ffc26a56dec: 8
A stored at address 0x7ffc26a56dec: 9
B::i is now dead. The memory location might contain 10 still.
So as you can a see, the i
in A dies early on and the i
in B happens to inherit its place in memory, hence the values. This is wildly undefined behavior because of the access to automatic storage duration variables after jumping around, but also since the function has finished executing.
If you use static int i
in both functions you might get the expected behavior but there are no guarantees.
setjmp/longjmp are surrounded by lots of undefined behavior and general best practice is to avoid them like the plague.
It looks like you are trying to create co-routines (user-space threads).
You are effectively saving and restoring the instruction pointer, but that's not enough. Each thread needs its own call stack and local variables ("stack"). Your two "threads" are clobbering each others local variables since they share the same stack.
To addresses this, you can the use ucontext "library" instead of long jumps.
gcc -O3 -g -std=c11 -Wall -Wextra -Werror -Wmissing-prototypes -Wstrict-prototypes sjlj83.c
), I get the message:sjlj83.c: In function ‘routine_a’:
——sjlj83.c:8:14: error: variable ‘i’ might be clobbered by ‘longjmp’ or ‘vfork’ [-Werror=clobbered]
——8 | for (int i = 0; i < 10; ++i)
—— and similarly forroutine_b()
. I changed the functions and file-scope variables so they were all static (exceptmain()
of course), and I used(void)
instead of()
for the function parameter lists. – Jonathan Leffler Commented Jan 15 at 15:25setjmp()
andlongjmp()
in the standard (C11 §7.13 Non-local jumps<setjmp.h>
are quite stringent. I note that Clang doesn't have a-Wclobbered
option under that name and doesn't report the problem that GCC does report. – Jonathan Leffler Commented Jan 15 at 15:25