Examples

Let’s see how if, else if and else, for loop, while loop and do while loop exist.

Example 1: if-else

#include <stdio.h>

int main(void) {
  int a = 5, b = 6;
  if (a > b){
    printf("Yes, 5 > 6.\n");
  }
  else{
    printf("No, 5 < 6.\n");
  }
}

Assembly:

	.text
	.section	.rodata
.LC0:
	.string	"Yes, 5 > 6."
.LC1:
	.string	"No, 5 < 6."

	.text
	.globl	main
	.type	main, @function
main:
	push	rbp
	mov	rbp, rsp
	sub	rsp, 16

	mov	DWORD PTR -4[rbp], 5              ; a
	mov	DWORD PTR -8[rbp], 6              ; b

	mov	eax, DWORD PTR -4[rbp]
	cmp	eax, DWORD PTR -8[rbp]            ; if (a > b)
	jle	.L2

	; if block
	lea	rax, .LC0[rip]
	mov	rdi, rax
	call	puts@PLT
	jmp	.L3

; else block
.L2:
	lea	rax, .LC1[rip]
	mov	rdi, rax
	call	puts@PLT

; return
.L3:
	mov	eax, 0
	leave
	ret

Example 2: if- else if -else

#include <stdio.h>

int main(void) {
  int a = 5, b = 6;
  if (a == b){
    printf("Yes, a > b.\n");
  }
  else if (a == b){
    printf("Actually, a == b.\n");
  }
  else{
    printf("No, a < b.\n");
  }
}

Assembly:

	.text
	.section	.rodata
.LC0:
	.string	"Yes, a > b."

.LC1:
	.string	"Actually, a == b."

.LC2:
	.string	"No, a < b."
	.text
	.globl	main
	.type	main, @function

main:
	push	rbp
	mov	rbp, rsp
	sub	rsp, 16

	mov	DWORD PTR -4[rbp], 5		; a
	mov	DWORD PTR -8[rbp], 6		; b

	mov	eax, DWORD PTR -4[rbp]
	cmp	eax, DWORD PTR -8[rbp]		; if (a > b)
	jne	.L2

	; if block
	lea	rax, .LC0[rip]
	mov	rdi, rax
	call	puts@PLT
	jmp	.L3

; else if block 
.L2:
	mov	eax, DWORD PTR -4[rbp]
	cmp	eax, DWORD PTR -8[rbp]		; if (a == b)
	jne	.L4
	lea	rax, .LC1[rip]
	mov	rdi, rax
	call	puts@PLT
	jmp	.L3

; else block
.L4:
	lea	rax, .LC2[rip]
	mov	rdi, rax
	call	puts@PLT

; return
.L3:
	mov	eax, 0
	leave
	ret

Example 3: Switch Case

#include <stdio.h>

int main(void) {
  int choice = 5;

  switch(choice){
    case 0:
      printf("Yes, equal to zero.\n");
      break;
    case 5:
      printf("No, not 1.\n");
      break;
    default:
      printf("No case matched. It is 5.\n");
  }
}

Assembly:

	.text
	.section	.rodata
.LC0:
	.string	"Yes, equal to zero."
.LC1:
	.string	"No, not 1."
.LC2:
	.string	"No case matched. It is 5."

	.text
	.globl	main
	.type	main, @function
main:
	push	rbp
	mov	rbp, rsp
	sub	rsp, 16
	mov	DWORD PTR -4[rbp], 5          ; choice

	cmp	DWORD PTR -4[rbp], 0          ; case 0
	je	.L2
	cmp	DWORD PTR -4[rbp], 5          ; case 1
	je	.L3
	jmp	.L7                           ; default

; case 0
.L2:
	lea	rax, .LC0[rip]
	mov	rdi, rax
	call	puts@PLT
	jmp	.L5

; case 1
.L3:
	lea	rax, .LC1[rip]
	mov	rdi, rax
	call	puts@PLT
	jmp	.L5

; default
.L7:
	lea	rax, .LC2[rip]
	mov	rdi, rax
	call	puts@PLT

; return
.L5:
	mov	eax, 0
	leave
	ret

Notice that for default case we are using unconditional jump.

  • Although .L7 is not strictly required but the semantics of switch cases require you to create a separate label for each. The compiler may optimize it at higher levels.

Example 4: Importance of break;

This code doesn’t use break; in case 5. We know that if break is not present, the cases after it are executed without any check. Let’s see.

#include <stdio.h>

int main(void) {
  int choice = 5;

  switch(choice){
    case 0:
      printf("Yes, equal to zero.\n");
      break;
    case 5:
      printf("No, not 1.\n");
    default:
      printf("No case matched. It is 5.\n");
  }
}

Assembly:

	.text
	.section	.rodata
.LC0:
	.string	"Yes, equal to zero."
.LC1:
	.string	"No, not 1."
.LC2:
	.string	"No case matched. It is 5."
	.text
	.globl	main
	.type	main, @function

main:
	push	rbp
	mov	rbp, rsp
	sub	rsp, 16
	mov	DWORD PTR -4[rbp], 5

	cmp	DWORD PTR -4[rbp], 0       ; case 0
	je	.L2
	cmp	DWORD PTR -4[rbp], 5       ; case 5
	je	.L3
	jmp	.L4

; case 0
.L2:
	lea	rax, .LC0[rip]
	mov	rdi, rax
	call	puts@PLT
	jmp	.L5

; case 5
.L3:
	lea	rax, .LC1[rip]
	mov	rdi, rax
	call	puts@PLT		   ; Notice, no jmp to .L5 

; case default
.L4:
	lea	rax, .LC2[rip]
	mov	rdi, rax
	call	puts@PLT

; return
.L5:
	mov	eax, 0
	leave
	ret

This code lacks an unconditional jump to .L5, the return label, which is why the case after it is executed without any check as the check is performed before.

Example 5: for loop

#include <stdio.h>

int main(void) {
  for (int i = 0; i < 5; i++){
    printf("i: %d\n", i);
  }
}

Assembly:

	.text
	.section	.rodata
.LC0:
	.string	"i: %d\n"

	.text
	.globl	main
	.type	main, @function
main:
	push	rbp
	mov	rbp, rsp
	sub	rsp, 16
	mov	DWORD PTR -4[rbp], 0          ; i = 0
	jmp	.L2

.L3:
	mov	eax, DWORD PTR -4[rbp]        ; load i
	mov	esi, eax                      ; esi = i (arg 2)
	lea	rax, .LC0[rip]
	mov	rdi, rax                      ; edi = addr(str)
	mov	eax, 0
	call	printf@PLT
	add	DWORD PTR -4[rbp], 1          ; update local instance of i

; body
.L2:
	cmp	DWORD PTR -4[rbp], 4        ; check i < 4 
	jle	.L3

	; otherwise, return
	mov	eax, 0
	leave
	ret

Notice that .L2 is present after .L3 , so .L2 need not to make any jump to it. That’s compiler optimization at bare minimum.

Even if you declare i outside of loop, nothing will change.

Example 6: Infinite for loop

#include <stdio.h>

int main(void) {
  int i = 0;
  for (;;){
    printf("i: %d\n", i);
  }
}

Assembly:

	.text
	.section	.rodata
.LC0:
	.string	"i: %d\n"

	.text
	.globl	main
	.type	main, @function
main:
	push	rbp
	mov	rbp, rsp
	sub	rsp, 16

	mov	DWORD PTR -4[rbp], 0
.L2:
	mov	eax, DWORD PTR -4[rbp]
	mov	esi, eax
	lea	rax, .LC0[rip]
	mov	rdi, rax
	mov	eax, 0
	call	printf@PLT
	jmp	.L2

Unless the source has an explicit break condition, it will run until it eats up all the resources.

Example 7: while loop

Although this is an infinite while loop, but it will not run.

#include <stdio.h>

int main(void) {

  int i = 0;
  while (i){
    printf("i: %d\n", i);
  }
}

Assembly:

	.text
	.section	.rodata
.LC0:
	.string	"i: %d\n"

	.text
	.globl	main
	.type	main, @function
main:
	push	rbp
	mov	rbp, rsp
	sub	rsp, 16
	mov	DWORD PTR -4[rbp], 0
	jmp	.L2
.L3:
	mov	eax, DWORD PTR -4[rbp]
	mov	esi, eax
	lea	rax, .LC0[rip]
	mov	rdi, rax
	mov	eax, 0
	call	printf@PLT
.L2:
	cmp	DWORD PTR -4[rbp], 0
	jne	.L3
	mov	eax, 0
	leave
	ret

Notice the condition in .L2. It jumps to .L3 when the comparison is not equal to zero.

Example 8: Infinite while loop

#include <stdio.h>

int main(void) {

  int i = 1;
  while (i){
    printf("i: %d\n", i);
  }
}

Assembly:

	.text
	.section	.rodata
.LC0:
	.string	"i: %d\n"

	.text
	.globl	main
	.type	main, @function
main:
	push	rbp
	mov	rbp, rsp
	sub	rsp, 16

 	mov	DWORD PTR -4[rbp], 1
	jmp	.L2
.L3:
	mov	eax, DWORD PTR -4[rbp]
	mov	esi, eax
	lea	rax, .LC0[rip]
	mov	rdi, rax
	mov	eax, 0
	call	printf@PLT
.L2:
	cmp	DWORD PTR -4[rbp], 0
	jne	.L3
	mov	eax, 0
	leave
	ret

If you notice, infinite while loop and infinite for loop matches instruction by instruction except the .L2 label in while loop, which basically checks for true/false.

Example 9: Finite while loop

#include <stdio.h>

int main(void) {

  int i = 0;
  while (i < 5){
    printf("i: %d\n", i);
  }
}

Assembly:

	.text
	.section	.rodata
.LC0:
	.string	"i: %d\n"

	.text
	.globl	main
	.type	main, @function
main:
	push	rbp
	mov	rbp, rsp
	sub	rsp, 16
	mov	DWORD PTR -4[rbp], 0
	jmp	.L2

.L3:
	mov	eax, DWORD PTR -4[rbp]
	mov	esi, eax
	lea	rax, .LC0[rip]
	mov	rdi, rax
	mov	eax, 0
	call	printf@PLT
	add	DWORD PTR -4[rbp], 1

.L2:
	cmp	DWORD PTR -4[rbp], 4
	jle	.L3

	; return
	mov	eax, 0
	leave
	ret

Again, it is exactly same as finite for loop.

This proves that for and while is just syntactic sugar.

Example 10: do while loop

#include <stdio.h>

int main(void) {
  int i = 0;
  do {
    printf("i: %d\n", i);
    i++ ;
  } while (i < 5);
}

Assembly:

	.text
	.section	.rodata
.LC0:
	.string	"i: %d\n"

	.text
	.globl	main
	.type	main, @function
main:
	push	rbp
	mov	rbp, rsp
	sub	rsp, 16
	mov	DWORD PTR -4[rbp], 0

.L2:
	mov	eax, DWORD PTR -4[rbp]
	mov	esi, eax
	lea	rax, .LC0[rip]
	mov	rdi, rax
	mov	eax, 0
	call	printf@PLT
	add	DWORD PTR -4[rbp], 1

	cmp	DWORD PTR -4[rbp], 4          ; while (i < 5) check
	jle	.L2

	; return
	mov	eax, 0
	leave
	ret

Conclusion

This is how control flow looks like.

Last updated on