Back to Home

Simple x86 assembler program: Sieve of Eratosthenes

assembler · x86 · primes · sieve of eratosthenes

Simple x86 assembler program: Sieve of Eratosthenes

  • Tutorial

introduction


In my profession, I do not encounter low-level programming: I am engaged in programming in scripting languages. But since the soul requires diversity, broadening the horizons of knowledge or just understanding how the machine works at a low level, I do programming in languages ​​other than those with which I make money - this is my hobby.

And now, I would like to share the experience of creating a simple assembly language program for x86 family processors, with the analysis of which you can begin your journey into conquering the lowlands of abstraction levels.

Before writing it, I formulated such requirements for a future program:
  • My program should not be a DOS program. Too many examples are oriented to it in connection with a simple API. My program must have run on modern OSs.
  • The program should use a bunch - get at its disposal dynamically allocated memory.
  • In order not to be too complicated, the program should work with unsigned integers without the use of hyphens.

For my program, I chose to search for primes using the Sieve of Eratosthenes . I chose nasm as the assembler .

I wrote the code with more emphasis on style and comprehensibility than on the speed of its execution. For example, I didn’t reset the register with help xor eax, eax, but with help mov eax, 0in connection with a more suitable instruction semantics. I decided that since the program pursues exclusively educational goals, you can unbelieve and engage in the pursuit of code style in assembler.

So, let's see what happened.

Where to begin?


Perhaps the most difficult thing that you encounter in the transition from high-level languages ​​to assembler is the organization of memory. Fortunately, there was already a good article on this topic on Habré .

The question also arises of how the data exchange between the internal world of the program and the external environment is realized at such a low level. Here the API of the operating system enters the scene. In DOS, as already mentioned, the interface was quite simple. For example, the program “Hello, world” looked like this:

SECTION .text
    org 0x100
    mov ah, 0x9
    mov dx, hello
    int 0x21
    mov ax, 0x4c00
    int 0x21
SECTION .data
    hello: db "Hello, world!", 0xD, 0xA, '$'


In Windows, the Win32 API is used for these purposes, respectively, the program should use the methods of the corresponding libraries:

%include "win32n.inc"
extern MessageBoxA
import MessageBoxA user32.dll
extern ExitProcess
import ExitProcess kernel32.dll
SECTION code use32 class=code
    ..start:
    push UINT MB_OK
    push LPCTSTR window_title
    push LPCTSTR banner
    push HWND NULL
    call [MessageBoxA]
    push UINT NULL
    call [ExitProcess]
SECTION data use32 class=data
    banner: db "Hello, world!", 0xD, 0xA, 0
    window_title: db "Hello", 0


Here we use the win32n.inc file , where macros are defined that reduce the code for working with the Win32 API.

I decided not to use the OS API directly and chose the way to use functions from the C library. It also opened up the possibility of compiling the program in Linux (and, most likely, in other OSs) - an achievement that is not too big and necessary for this program, but a pleasant achievement.

Subroutine call


The need to call subroutines entails several topics for study: organizing subroutines, passing arguments, creating a stack frame, working with local variables.

Subprograms are the label by which the code is located. The subroutine ends with an instruction ret. For example, here is a subroutine in DOS that displays the string “Hello, world” in the console:

print_hello:
    mov ah, 0x9
    mov dx, hello
    int 0x21
    ret


To call it, you would need to use the instruction call:

call print_hello


For myself, I decided to pass arguments to subprograms through registers and indicate in the comments in which registers which arguments should be, but in high-level languages, arguments are passed through the stack. For example, this is how the function printf from the C library is called :

push hello
call _printf
add esp, 4


Arguments are passed from right to left, the responsibility to clear the stack lies with the caller.

When entering the subroutine, you need to create a new stack frame. This is done as follows:

print_hello:
    push ebp ;сохраняем указатель начала стекового кадра на стеке
    mov ebp, esp ;теперь началом кадра является вершина предыдущего


Accordingly, before exiting, you need to restore the previous state of the stack:

    mov esp, ebp
    pop ebp
    ret


For local variables , a stack is also used on which, after creating a new frame, the required number of bytes is allocated:

print_hello:
    push ebp
    mov ebp, esp
    sub esp, 8 ;опускаем указатель вершины стека на 8 байт, чтобы выделить память


The x86 architecture also provides special instructions with the help of which these actions can be more succinctly implemented:

print_hello:
    enter 8, 0 ;создать новый кадр, выделить 8 байт для локальных переменных
    leave ;восстановить стек
    ret


The second parameter of the instruction enteris the nesting level of the subroutine. It is needed for linking with high-level languages ​​that support such a technique for organizing routines. In our case, this value can be left zero.

The program itself


The project contains the following files:
  • main.asm - main file,
  • functions.asm - routines
  • string_constants.asm - definitions of string constants,
  • Makefile - build script

Consider the code for the main file:

main.asm
%define SUCCESS 0
%define MIN_MAX_NUMBER 3
%define MAX_MAX_NUMBER 4294967294
global _main
extern _printf
extern _scanf
extern _malloc
extern _free
SECTION .text
_main:
	enter 0, 0
	;ввод максимального числа
	call input_max_number
	cmp edx, SUCCESS
	jne .custom_exit
	mov [max_number], eax
	;выделяем память для массива флагов
	mov eax, [max_number]
	call allocate_flags_memory
	cmp edx, SUCCESS
	jne .custom_exit
	mov [primes_pointer], eax
	;отсеять составные числа
	mov eax, [primes_pointer]
	mov ebx, [max_number]
	call find_primes_with_eratosthenes_sieve
	;вывести числа
	mov eax, [primes_pointer]
	mov ebx, [max_number]
	call print_primes
	;освободить память от массива флагов
	mov eax, [primes_pointer]
	call free_flags_memory
	;выход
	.success:
		push str_exit_success
		call _printf
		jmp .return
	.custom_exit:
		push edx
		call _printf
	.return:
		mov eax, SUCCESS
		leave
		ret
	%include "functions.asm"
SECTION .data
	max_number: dd 0
	primes_pointer: dd 0
	%include "string_constants.asm"


It can be seen that the program is divided in meaning into 5 blocks, designed as subprograms:
  1. input_max_number- using the console it asks the user for the maximum number to which simple ones are searched; to avoid errors, the value is limited by constants MIN_MAX_NUMBERandMAX_MAX_NUMBER
  2. allocate_flags_memory- request from the OS the allocation of memory for the array of tagging numbers (simple / compound) on the heap; if successful, returns a pointer to the allocated memory through the registereax
  3. find_primes_with_eratosthenes_sieve - weed out composite numbers using the classic sieve of Eratosthenes;
  4. print_primes - display in the console a list of primes;
  5. free_flags_memory - free up memory allocated for flags

For functions, this rule was agreed: the value is returned through the register eax, the register edxcontains the status. In case of success, it contains a value SUCCESS, that is, 0in case of failure, the address of the line with the error message that will be displayed to the user.

The file string_constants.asmcontains a definition of string variables whose values, as the name of the file hints, are not intended to be changed. It was only for the sake of these variables that an exception was made to the “do not use global variables” rule. I never found a more convenient way to deliver string constants to I / O functions - I even thought about writing to the stack immediately before function calls, but decided that this idea is much worse than the idea with global variables.

string_constants.asm
;подписи ввода-вывода, форматы
str_max_number_label: db "Max number (>=3): ", 0
str_max_number_input_format: db "%u", 0
str_max_number_output_format: db "Using max number %u", 0xD, 0xA, 0
str_print_primes_label: db "Primes:", 0xD, 0xA, 0
str_prime: db "%u", 0x9, 0
str_cr_lf: db 0xD, 0xA, 0
;сообщения выхода
str_exit_success: db "Success!", 0xD, 0xA, 0
str_error_max_num_too_little: db "Max number is too little!", 0xD, 0xA, 0
str_error_max_num_too_big: db "Max number is too big!", 0xD, 0xA, 0
str_error_malloc_failed: db "Can't allocate memory!", 0xD, 0xA, 0


The following scenario is used for assembly:

Makefile
ifdef SystemRoot
   format = win32
   rm = del
   ext = .exe
else
   format = elf
   rm = rm -f
   ext = 
endif
all: primes.o
	gcc primes.o -o primes$(ext)
	$(rm) primes.o
primes.o:
	nasm -f $(format) main.asm -o primes.o



Subprograms (functions)


input_max_number

Subroutine code
; Ввести максимальное число
; Результат: EAX - максимальное число
input_max_number:	
	;создать стек-фрейм,
	;4 байта для локальных переменных
	enter 4, 1
	;показываем подпись
	push str_max_number_label ;см. string_constants.asm
	call _printf
	add esp, 4
	;вызываем scanf
	mov eax, ebp
	sub eax, 4
	push eax
	push str_max_number_input_format ;см. string_constants.asm
	call _scanf
	add esp, 8
	mov eax, [ebp-4]
	;проверка
	cmp eax, MIN_MAX_NUMBER
	jb .number_too_little
	cmp eax, MAX_MAX_NUMBER
	ja .number_too_big
	jmp .success
	;выход
	.number_too_little:
		mov edx, str_error_max_num_too_little ;см. string_constants.asm
		jmp .return	
	.number_too_big:
		mov edx, str_error_max_num_too_big ;см. string_constants.asm
		jmp .return	
	.success:
		push eax
		push str_max_number_output_format ;см. string_constants.asm
		call _printf
		add esp, 4
		pop eax
		mov edx, SUCCESS
	.return:
		leave
		ret


The subprogram is intended to introduce into the program the maximum number up to which simple ones will be searched. The key point here is the function call scanffrom the C library:

        mov eax, ebp
        sub eax, 4
        push eax
        push str_max_number_input_format ;см. string_constants.asm
        call _scanf
        add esp, 8
        mov eax, [ebp-4]

Thus, eaxthe memory address is written 4 bytes below the stack base pointer first. This is the memory allocated for the local needs of the subprogram. A pointer to this memory is passed to the function scanfas the target for writing data entered from the keyboard.

After calling the function, the eaxentered value is moved to the memory.

allocate_flags_memory and free_flags_memory

Routine code
; Выделить память для массива флагов
; Аргумент: EAX - максимальное число
; Результат: EAX - указатель на память
allocate_flags_memory:
	enter 8, 1
	;выделить EAX+1 байт
	inc eax
	mov [ebp-4], eax
	push eax
	call _malloc
	add esp, 4
	;проверка
	cmp eax, 0
	je .fail
	mov [ebp-8], eax
	;инициализация
	mov byte [eax], 0
	cld
	mov edi, eax
	inc edi
	mov edx, [ebp-4]
	add edx, eax
	mov al, 1
	.write_true:
		stosb
		cmp edi, edx
		jb .write_true
	;выход
	mov eax, [ebp-8]
	jmp .success
	.fail:
		mov edx, str_error_malloc_failed ;см. string_constants.asm
		jmp .return
	.success:
		mov edx, SUCCESS
	.return:
		leave
		ret
; Освободить память от массива флагов
; Аргумент: EAX - указатель на память
free_flags_memory:
	enter 0, 1
	push eax
	call _free
	add esp, 4
	leave
	ret


Key locations of these routines are the functions of the challenges mallocand freeof the C library.

mallocin case of success, returns the eaxaddress of the allocated memory through the register , in case of failure, this register contains 0. This is the bottleneck of the program regarding the maximum number. 32 bits is enough to search for primes up to 4,294,967,295, but you won’t be able to allocate as much memory at once.

find_primes_with_eratosthenes_sieve

Subroutine code
;Найти простые числа с помощью решета Эратосфена
;Аргументы: EAX - указатель на массив флагов, EBX - максимальное число	
find_primes_with_eratosthenes_sieve:
	enter 8, 1
	mov [ebp-4], eax
	add eax, ebx
	inc eax
	mov [ebp-8], eax
	;вычеркиваем составные числа
	cld
	mov edx, 2 ;p = 2
	mov ecx, 2 ;множитель с = 2
	.strike_out_cycle:
		;x = c*p
		mov eax, edx
		push edx
		mul ecx
		pop edx
		cmp eax, ebx
		jbe .strike_out_number
		jmp .increase_p
		.strike_out_number:
			mov edi, [ebp-4]
			add edi, eax
			mov byte [edi], 0
			inc ecx ;c = c + 1
			jmp .strike_out_cycle
		.increase_p:
			mov esi, [ebp-4]
			add esi, edx
			inc esi
			mov ecx, edx
			inc ecx
			.check_current_number:
				mov eax, ecx
				mul eax
				cmp eax, ebx
				ja .return
				lodsb
				inc ecx
				cmp al, 0
				jne .new_p_found
				jmp .check_current_number
				.new_p_found:
					mov edx, ecx
					dec edx
					mov ecx, 2
					jmp .strike_out_cycle			
	.return:
		leave
		ret



The subroutine implements the classical algorithm for striking out composite numbers, the sieve of Eratosthenes, in x86 assembly language. It is pleasant because it does not use external function calls and does not require error handling :)

print_primes

Subroutine code
; Вывести простые числа
; Параметры: EAX - указатель на массив флагов, EBX - максимальное число
print_primes:
	enter 12, 1
	mov [ebp-4], eax
	mov [ebp-8], ebx
	push str_print_primes_label
	call _printf
	add esp, 4
	cld
	mov esi, [ebp-4]
	mov edx, esi
	add edx, [ebp-8]
	inc edx
	mov [ebp-12], edx
	mov ecx, 0
	.print_cycle:
		lodsb
		cmp al, 0
		jne .print
		jmp .check_finish
		.print:
			push esi
			push ecx
			push str_prime ;см. string_constants.asm
			call _printf
			add esp, 4
			pop ecx
			pop esi
			mov edx, [ebp-12]
		.check_finish:
			inc ecx
			cmp esi, edx
			jb .print_cycle
	push str_cr_lf
	call _printf
	add esp, 4
	leave
	ret


The routine prints prime numbers to the console. The key point here is the function call printffrom the C library.

Conclusion


Well, the program meets all the stated requirements and seems easy to understand. I would like to hope that someone’s analysis of it will help to understand programming at a low level and he will enjoy it as much as I did.

I also bring the complete source code of the program .

I can also give an interesting fact. Since we were taught from childhood that assembler programs run faster, I decided to compare the speed of this program with the speed of a C ++ program that I wrote once and was looking for primes using the Atkin Reshet . A C ++ program compiled in Visual Studio c /O2searched up to 2 30in about 25 seconds on my car. The program in assembler showed 15 seconds with the Sieve of Eratosthenes.

This, of course, is more a trick than a scientific fact, since there was no serious testing, there was no clarification of the reasons, but as an interesting fact, it seems to me to complete the article.

useful links


  1. List of Resources for Learning Assembler
  2. Memory organization
  3. Sieve of Eratosthenes
  4. Atkin sieve
  5. Stack
  6. Stack frame

Read Next