Page 1 of 1

NASM/Windows - Let's create a pyramid of characters!

#1 GunnerInc  Icon User is offline

  • "Hurry up and wait"
  • member icon




Reputation: 858
  • View blog
  • Posts: 2,302
  • Joined: 28-March 11

Post icon  Posted 01 December 2012 - 04:21 PM

You will see this question from time to time: "How do I create a pyramid/diamond out of (some character here) in (your language here)?" Do you wonder why this task is given? It teaches you how to use loops. A loop is a widely used way to repeat a task.

A sample of a Pyramid of Characters:
                       A

                     ppppp
                    rrrrrrr
                   yyyyyyyyy
                  aaaaaaaaaaa
                 mmmmmmmmmmmmm
                iiiiiiiiiiiiiii
               ddddddddddddddddd

             ooooooooooooooooooooo
            fffffffffffffffffffffff

          ccccccccccccccccccccccccccc
         hhhhhhhhhhhhhhhhhhhhhhhhhhhhh
        aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
       rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr
      aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
     ccccccccccccccccccccccccccccccccccccc
    ttttttttttttttttttttttttttttttttttttttt
   eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
  rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr
 sssssssssssssssssssssssssssssssssssssssssssss
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

So how would you code this? Open up a text editor/IDE that shows line and column numbers, type in a sample pyramid and study it. What do you notice? If I type in a pyramid consisting of "abcde":
    a
   bbb
  ccccc
 ddddddd
eeeeeeeee

Here is what I notice:
  • The length of the entered string is 5
  • The start pos of the first character equals the string length and decreases on each line.
  • The amount of spaces on the first line is (string length - 1) and decreases on each line.
  • The amount each character repeats on each line equals (current line number + previous line number)
  • The amount of lines to print is the same as the entered strings length
  • The length of the last line is (entered string length * 2 - 1)


Now, let's enter a string and create a pyramid. WAIT, not so fast! Start slow, break it down! Get the entered string into a buffer and loop through each character and print each one on a separate line.
ShowPyramid:                                
%define _CharStartPos dword [ebp - 4]       
%define _CharsToPrint dword [ebp - 8]       
%define _CurLine dword [ebp - 12]           
%define _Pyramid dword [ebp - 16]           
%define _Dummy  [ebp - 20]                  
                                            
    ;###### prologue #######################
    push    ebp                             
    mov     ebp, esp                        
    sub     esp, 4 * 5                      
                                            
    mov     eax, dword [lpNumRead]                                                    
    add     eax, eax                        
                        
    push    eax                             
    push    HEAP_ZERO_MEMORY                
    push    dword [hHeap]                   
    call    HeapAlloc                       
    mov     _Pyramid, eax                   
                                            
    ;###### initialize vars to 1            
    mov     _CurLine, 1                     
    mov     esi, buffer                     
                                            
.NextLine:                                  
    mov     edi, _Pyramid                   
    mov     al, byte [esi]                                                
    mov     byte [edi], al                                     
    mov     byte [edi + 1], 10
                                            
    lea     eax, _Dummy                     
    push    NULL                            
    push    eax                             
    push    2                               
    push    _Pyramid                        
    push    dword [stdout]                  
    call    WriteFile                       
                                            
    inc     _CurLine                        
    inc     esi                             
    mov     eax, dword [lpNumRead]          
    cmp     _CurLine, eax                   
    jng     .NextLine                       
                                            
    push    _Pyramid                        
    push    NULL                            
    push    dword [hHeap]                   
    call    HeapFree                        
                                            
    ;###### eplilogue ######################
    add     esp, 4 * 5                      
    mov     esp, ebp                        
    pop     ebp                             
    ret     

This will get the size of entered string that was returned from ReadFile - lpNumRead and double the size to hold the last line, create a buffer to hold it, loop through entered string, move one character into the _Pyramid buffer, tack on a Linefeed char (10), print it, check to see if we are on the last line, if not loop and do next char.
ShowPyramid:                                
%define _CharStartPos dword [ebp - 4]       
%define _CharsToPrint dword [ebp - 8]       
%define _CurLine dword [ebp - 12]           
%define _Pyramid dword [ebp - 16]           
%define _Dummy  [ebp - 20]                  
                                            
    ;###### prologue #######################
    push    ebp                             
    mov     ebp, esp                        
    sub     esp, 4 * 5                      
                                            
    mov     eax, dword [lpNumRead]          
                                            
    add     eax, eax                        
    add     eax, 2                          
    push    eax                             
    push    HEAP_ZERO_MEMORY                
    push    dword [hHeap]                   
    call    HeapAlloc                       
    mov     _Pyramid, eax                   
                                            
    ;###### initialize vars to 1            
    mov     _CurLine, 1

We will use 5 variables on the stack. I like to use named variables, which make code a bit self documenting, so we will tell NASM that anywhere it sees the word after %define, to replace it with with dword [ebp + #] . Anywhere it sees _CharStartPos, it will replace it with dword [ebp - 4]

Next we use setup the stack and make room for our 5 variables with sub esp, 4 * 5.
One of the things I noticed was:

Quote

The length of the last line is (entered string length * 2 - 1)

So, we will take the entered string length and double it which will leave room for a linefeed char, and save the returned pointer to our local var - _Pyramid
    mov     eax, dword [lpNumRead]          
                                            
    add     eax, eax                        
    add     eax, 2                          
    push    eax                             
    push    HEAP_ZERO_MEMORY                
    push    dword [hHeap]                   
    call    HeapAlloc                       
    mov     _Pyramid, eax   

We are not using (entered string length * 2 - 1), but instead (entered string length * 2) We don't need the buffer to be (entered string length * 2) just to print 1 character on each line, but it is done and out of the way.
We need loop counter to know when to stop printing, so we will use _CurLine and initialize it to 1.
    mov     esi, buffer                     
                                            
.NextLine:                                  
    mov     edi, _Pyramid                   
    mov     al, byte [esi]                                                
    mov     byte [edi], al                                     
    mov     byte [edi + 1], 10
                                            
    lea     eax, _Dummy                     
    push    NULL                            
    push    eax                             
    push    2                               
    push    _Pyramid                        
    push    dword [stdout]                  
    call    WriteFile                       
                                            
    inc     _CurLine                        
    inc     esi                             
    mov     eax, dword [lpNumRead]          
    cmp     _CurLine, eax                   
    jng     .NextLine                       
                                            
    push    _Pyramid                        
    push    NULL                            
    push    dword [hHeap]                   
    call    HeapFree                        
                                            
    ;###### eplilogue ######################
    add     esp, 4 * 5                      
    mov     esp, ebp                        
    pop     ebp                             
    ret     

We setup esi to contain the pointer to our entered text buffer
edi will contain the pointer to the buffer that holds the line to print.
Get a byte from our input buffer, copy it to our line buffer, and tack on a linefeed char
.NextLine:                                  
    mov     edi, _Pyramid                   
    mov     al, byte [esi]                                                
    mov     byte [edi], al                                     
    mov     byte [edi + 1], 10

Print the character to console:
    lea     eax, _Dummy                     
    push    NULL                            
    push    eax                             
    push    2                               
    push    _Pyramid                        
    push    dword [stdout]                  
    call    WriteFile   

    inc     _CurLine                        
    inc     esi                             
    mov     eax, dword [lpNumRead]          
    cmp     _CurLine, eax                   
    jng     .NextLine  

increase our entered text buffer pointer with inc esi. Say what?!?! Ok, pointers go like this:
We entered 5 characters - abcde and stored it in our buffer called buffer. When we run our program, buffer starts at address 123456 and ends at address 123516 (addresses just for example), we reserved 60 bytes for this buffer in our .bss section with buffer resb MAX_CHARS_TO_READ where MAX_CHARS_TO_READ is defined as %define MAX_CHARS_TO_READ 60 hence a 60 byte address spread.
When we first enter the loop, esi contains the address 123456 which contains the letter a. Then we increase the pointer by one with inc esi so now esi contains the address 123457 which the letter b is at.
esi 123456 "a"
inc esi
esi 123457 "b"
inc esi
esi 123458 "c"
inc esi
esi 123459 "d"
inc esi
esi 123460 "e"

increase our current line counter inc _CurLine and check to see if it is more than the entered string length, if not loop back and do next character. Which takes care of observation:

Quote

The amount of lines to print is the same as the entered strings length

Attached Image
Ok, so that is the loop to go through each entered character and print each one on a line. Next we expand it to repeat each character a certain amount of times

Quote

The amount each character repeats on each line equals (current line number + previous line number)

    ;###### initialize vars to 1            
    mov     _CurLine, 1                     
    mov     _CharsToPrint, 1                
                                            
    mov     esi, buffer                     
                                            
.NextLine:                                  
    mov     edi, _Pyramid                   
                                            
    mov     ecx, _CharsToPrint              
    mov     al, byte [esi]                  
                                            
.FillLine:                                  
    mov     byte [edi], al                  
    inc     edi                             
    dec     ecx                             
    jnz     .FillLine                       
                                            
    mov     edi, _Pyramid                   
    call    StrLen                          
                                            
    lea     eax, _Dummy                     
    push    NULL                            
    push    eax                             
    push    ecx                             
    push    _Pyramid                        
    push    dword [stdout]                  
    call    WriteFile                       
                                            
    mov     eax, _CurLine                   
    inc     _CurLine                        
    add     eax, _CurLine                   
    mov     _CharsToPrint, eax              
                                            
    inc     esi                             
    mov     eax, dword [lpNumRead]          
    cmp     _CurLine, eax                   
    jng     .NextLine                       


We will use the variable _CharsToPrint to keep track of how many characters to print. Somethings were added/changed/removed from this loop. We also added another loop called FillLine
.FillLine:                                  
    mov     byte [edi], al                  
    inc     edi                             
    dec     ecx                             
    jnz     .FillLine 

On the first loop of NextLine, _CharsToPrint is 1, so this loop will just add 1 character to our _Pyramid buffer. This will take the character we got at the current pointer index in esi, copy it into the current pointer index in edi and decrease ecx and loop until it is zero. Next we get the length of the string to print (The StrLen function also tacks on a linefeed for us) and prints out each repeated character on a line.

So how do we figure out how many times to repeat the character on the line?

Quote

The amount each character repeats on each line equals (current line number + previous line number)

    mov     eax, _CurLine                   
    inc     _CurLine                        
    add     eax, _CurLine                   
    mov     _CharsToPrint, eax  

We move the _CurLine count into eax, advance the _CurLine (Which makes the variable the "Next line", add the "Next Line" to eax to give us the total amount of chars to print on the next line.
Attached Image

Now that we wrote the code to deal with 4 of the 6 observations, now we continue on to add the correct amount of spaces to each line.
  • The length of the entered string is 5
  • The start pos of the first character equals the string length and decreases on each line.
  • The amount of spaces on the first line is (string length - 1) and decreases on each line.
  • The amount each character repeats on each line equals (current line number + previous line number)
  • The amount of lines to print is the same as the entered strings length
  • The length of the last line is (entered string length * 2 - 1)


We need to add 2 labels 1 more loop, and 11 instructions to accomplish #2 and #3.

Towards the top of ShowPyramid, right after the prologue, we save the entered string length to _CharStartPos
    mov     eax, dword [lpNumRead]
    mov     _CharStartPos, eax 

Now that we have that, we can print the correct number of spaces in front of the repeated characters.
The beginning of the NextLine loop gets changed to:
.NextLine:                     
    mov     edi, _Pyramid      
    mov     edx, _CharStartPos 
    dec     edx                
    test    edx, edx           
    jz      .SpaceDone                   
                               
.FillSpaces:                   
    mov     byte [edi], 32     
    inc     edi                
    dec     edx                
    jnz     .FillSpaces        
                        
.SpaceDone:                    
    mov     ecx, _CharsToPrint 

We take the value in of _CharStartPos, move it into edx, decrease the value in edx by one, and check to see if it is zero. If it is zero, we skip the FillSpaces loop. Now edx contains the amount of spaces to add, we will use this as our counter in the FillSpaces loop. We move the space character (32) into the pointer index in edi increase our pointer and keep repeating until edx == 0. Once we are done adding spaces, our pointer will be incremented to the correct spot to add our character(s).
    a
   bbb 
  ccccc
 ddddddd
eeeeeeeee

First line, the letter "a". Entered string length == 5, start pos for "a" is 5, total spaces to print before "a" is 4.

Before we check to see if the NextLine loop should continue, we decrease the value in _CharStartPos
    dec     _CharStartPos         
    inc     esi                   
    mov     eax, dword [lpNumRead]
    cmp     _CurLine, eax         
    jng     .NextLine  


Attached Image

Complete code:
%include "externs.inc"

%define     MAX_CHARS_TO_READ 60            ; needs to be total chars wanted + 2
%define     STD_INPUT_HANDLE -10
%define     STD_OUTPUT_HANDLE -11
%define     NULL 0
%define     HEAP_ZERO_MEMORY 0x00000008

SECTION .data
szPrompt        db  "Ready to make an alphabet pyramid?", 13, 10
                db  "Enter some letters from the alphabet (58 max): ", 0
Prompt_Len      equ $-szPrompt

szExitPrompt    db  "Press any key to exit", 0
szfmt           db  "%s%s", 0
szfmt2          db  "%d%s", 0
szCRLF          db  13,10,0

SECTION .bss
buffer          resb MAX_CHARS_TO_READ
stdin           resd 1
stdout          resd 1
lpNumRead       resd 1
hHeap           resd 1

SECTION .text
main:
    ;~ Some initialization 
    push    STD_INPUT_HANDLE                ;
    call    GetStdHandle                    ; get handle of console input
    mov     [stdin], eax                    ; save handle
    
    push    STD_OUTPUT_HANDLE               ;
    call    GetStdHandle                    ; get handle of console output
    mov     [stdout], eax                   ; save it
    
    call    GetProcessHeap                  ;
    mov     [hHeap], eax                    ;
    
.ShowPrompt:
    push    NULL                            ;
    push    lpNumRead                       ;
    push    Prompt_Len                      ;
    push    szPrompt                        ;
    push    dword [stdout]                  ;
    call    WriteFile                       ; 
        
    push    NULL                            ;
    push    lpNumRead                       ;
    push    MAX_CHARS_TO_READ               ; 2 more than expected chars, CRLF tacked on
    push    buffer                          ; 
    push    dword [stdin]                   ;
    call    ReadFile                        ;
    
    mov     eax, dword [lpNumRead]          ; chars read
    sub     eax, 2                          ; subtract 2 from total read
    test    eax, eax                        ; is it zero, if so
    jz      .ShowPrompt                     ; go back and show prompt

    mov     dword [lpNumRead], eax          ; save total chars read - CRLF

    call    ShowPyramid                     ; display pyramid
        
    call    __getch                         ; To hell with you, it is a Windows app, no cross OS :-p

    ret

;~ #########################################
StrLen:
    xor     ecx, ecx
    not     ecx
    xor     eax, eax
    cld
    repne   scasb
    not     ecx                             ; length of string in ecx
    mov     byte [edi - 1], 10
    ret
        
;~ #########################################
ShowPyramid:
%define _CharStartPos dword [ebp - 4]
%define _CharsToPrint dword [ebp - 8]
%define _CurLine dword [ebp - 12]
%define _Pyramid dword [ebp - 16]
%define _Dummy  [ebp - 20]

    ;###### prologue #######################;
    push    ebp
    mov     ebp, esp
    sub     esp, 4 * 5                      ; make room on stack for 6 "local" vars
        
    mov     eax, dword [lpNumRead]          ;
    mov     _CharStartPos, eax              ; first char is printed at NumRead
    
    add     eax, eax                        ; double chars read total
    push    eax                             ; 
    push    HEAP_ZERO_MEMORY                ;
    push    dword [hHeap]                   ;
    call    HeapAlloc                       ; create buffer for longest line
    mov     _Pyramid, eax                   ; save pointer
    
    ;###### initialize vars to 1            ;
    mov     _CurLine, 1                     ;
    mov     _CharsToPrint, 1                ;
    
    mov     esi, buffer                     ; esi == pointer to inputed chars

.NextLine:
    mov     edi, _Pyramid                   ; edi == pointer to line to print;;;;;
    mov     edx, _CharStartPos              ; check to see if starting pos is 0
    dec     edx                             ; if it is, we don't need to add any spaces
    test    edx, edx                        ;
    jz      .SpaceDone                      ;
                                            ; num spaces == CharStartPos - 1

.FillSpaces:
    mov     byte [edi], 32                  ; add space char to our buffer
    inc     edi                             ; increase our buffer pointer index by 1
    dec     edx                             ; decrease our spaces to print counter
    jnz     .FillSpaces                     ; until 0
 
.SpaceDone:
    mov     ecx, _CharsToPrint              ; ecx == how many times to repeat char
    mov     al, byte [esi]                  ; get char to repeat
    
.FillLine:
    mov     byte [edi], al                  ; add char to our buffer at current pointer index
    inc     edi                             ; increase pointer index
    dec     ecx                             ; repeat while ecx != 0
    jnz     .FillLine                       ;

    mov     edi, _Pyramid                   ;
    call    StrLen                          ; get length of current line 
  
    lea     eax, _Dummy                     ;
    push    NULL                            ;
    push    eax                             ;
    push    ecx                             ;
    push    _Pyramid                        ;
    push    dword [stdout]                  ;
    call    WriteFile                       ; Write current line to console
    
    mov     eax, _CurLine                   ;
    inc     _CurLine                        ;
    add     eax, _CurLine                   ;
    mov     _CharsToPrint, eax              ; Chars to print == Current Line number + Prev Line Number

    dec     _CharStartPos                   ; Char start pos == Char start pos - 1
    inc     esi                             ; increase pointer index of chars to print
    mov     eax, dword [lpNumRead]          ; check to see if we are at last line
    cmp     _CurLine, eax                   ; if Cur line < total chars to print
    jng     .NextLine                       ; then do next line
    
    push    _Pyramid                        ;
    push    NULL                            ;
    push    dword [hHeap]                   ;
    call    HeapFree                        ; free our buffer

    ;###### eplilogue ######################;
    add     esp, 4 * 5                      ; 
    mov     esp, ebp                        ;
    pop     ebp                             ;
    ret                                     ; no parameters passed, just need ret


Attached Image
See, not too bad once you break it down and write the loop in parts.

Here is a challenge for you: Modify the code to create a diamond instead of a pyramid. Can it be done? Can you do it? Give it a try!

Attached File(s)



Is This A Good Question/Topic? 0
  • +

Page 1 of 1