## 1. Tiny introduction - Trong quá trình mình học code C, một vấn đề mình rất ghét phải đối mặt đó là tràn số. Điều này xuất phát từ việc dữ liệu của C chỉ gói gọn trong 64 bit, tức là số lớn nhất lưu được dưới dạng long long có giá trị đâu đó tầm 9 tỉ tỉ (`9 * 10^18`). Khi vượt qua giá trị này, việc tính toán trở nên khó khăn và nảy sinh nhiều lỗi. - Assembly cũng gặp vấn đề tương tự khi mặc định số lưu được trong thanh ghi EAX là 32 bit, RAX là 64 bit. Chính vì vậy, đối với những task khó như tìm số fibonacci (mà mình sẽ nói trong bài 105), chúng ta cần công cụ để handle các số lớn này (trong bài mình sẽ gọi là Big Num). ## 2. Algorithm - Một cách giải quyết số lớn điển hình và cổ điển mà mình gặp trong code C đó là sử dụng string để lưu giá trị. Khi đó, các phép toán cộng, trừ, nhân, chia, modulo sẽ không dùng được, mà chúng ta phải viết hàm để xử lí. Trong bài này mình chỉ nói về việc cộng hai số lớn với nhau. - Chúng ta sẽ lưu big num dưới dạng string, ví dụ số `3` sẽ được lưu thành `"3"` và `999` sẽ được lưu thành `"999"`. Sau đó mình sẽ thực hiện cộng từng chữ số từ phải sang trái, có lấy nhớ (thực hiện giống khi học tiểu học). ## 3. Code - Chi tiết thuật toán có thể tham khảo [nguồn này](https://www.geeksforgeeks.org/how-to-handle-large-numbers-in-c/) ### Reverse String - Với mình, việc `memmove` như trong code C khá sú, vì khi đó mình phải truy cập vào địa chỉ ngay trước big num của mình. Ví dụ như chuỗi `"3"` có địa chỉ 1, muốn `memmove` để nó trở thành `"03"` thì phải truy cập vào địa chỉ 0 để thêm `"0"`. Hoặc không cũng phải dịch các giá trị sang phải một đơn vị nhớ rồi mới thêm `"0"` được. - Điều này có thể làm thay đổi dữ liệu của các biến trước đó trong vùng nhớ, bên cạnh đó mình cũng ngại việc shifting, vậy nên mình xử lí bằng cách đảo ngược chuỗi và thêm `"0"` vào cuối chứ không phải đầu. Khi đó phép cộng cũng được thực hiện từ trái qua phải, từ `i = 0` cho tới `i = strlen(num)`, mà các giá trị trong bộ nhớ cũng được lưu từ trái qua phải nên thuận tiện hơn - Dưới đây là code để đảo ngược chuỗi mà mình đã code để quẳng vào file `functions.asm` (cần có hàm `sLen` của file vì mình custom hàm đó khác với của [asmtutor](https://asmtutor.com/)): ```assembly= ; void reverseStr(String message) ; Reverse a string ; Source: esi ; Destination: edi reverseStr: push eax push ecx push edi push esi push esi call sLen mov ecx, esi ; n dec ecx pop esi next_reverseStr: cmp ecx, -1 je finished_reverseStr mov al, byte [esi + ecx] mov [edi], al inc edi dec ecx jmp next_reverseStr finished_reverseStr: mov [edi], byte 0 ; make null pop esi pop edi pop ecx pop eax ret ``` - Hàm sẽ nhận vào `ESI` là địa chỉ của chuỗi ban đầu, `EDI` là địa chỉ của chuỗi đảo ngược, tức là `reverseStr(ESI) = EDI`. Test code: ```assembly= global _start %include "functions.asm" section .bss input: resb 100 output: resb 100 outoutput: resb 100 section .data msg1: db ">> ", 0 msg2: db "Your reversed input is: ", 0 newline: db "", 0xa msg3: db "Your reversed reversed input is: ", 0 section .text _start: ; Print msg1 lea esi, [msg1] call sPrint lea esi, [input] call sScan ; Print msg2 lea esi, [msg2] call sPrint ; Output: reversed input lea esi, [input] lea edi, [output] call reverseStr lea esi, [output] call sPrint ; Print newline mov edx, 1 lea ecx, [newline] mov ebx, 1 mov eax, 4 int 0x80 ; Outoutput: reversed output lea esi, [output] lea edi, [outoutput] call reverseStr lea esi, [msg3] call sPrint lea esi, [outoutput] call sPrint call quit ``` ![reverseStr](https://hackmd.io/_uploads/S1n8Hsskke.png) - Chuỗi đảo ngược lại đúng và chuỗi đảo ngược hai lần giống hệt chuỗi ban đầu, điều đó chứng tỏ hàm của mình có hiệu quả, bảo toàn được dữ liệu. Điều này giúp mình tránh lỗi khi cộng. Lấy ví dụ cộng `"3"` vào `"999"` như thuật toán ra được `"2001"`, khi đó cần đảo ngược lại chuỗi để được kết quả đúng thì hàm cho kết quả đúng như mong đợi. ### Adding Big Num - Code của mình khá ngắn, hàm nhận vào các tham số `num1, num2, reversedNum1, reversedNum2`, push theo đúng thứ tự. Tại đây mình thực hiện thuật toán giống như trong link trên, đảo ngược `num1` và `num2`, cộng hai số, lưu tổng (bị ngược) trong `reservedNum1` và kết quả cuối cùng nằm trong `num1`. - Dưới đây là hàm `addBigNum` của mình, trông hơi kinh dị, mình cũng mất mấy tiếng để ngồi fix sao cho ngon nhất, mặc dù chưa ưng ý lắm vì phải truyền `reservedNum1` và `reservedNum2` chả liên quan mẹ gì đến hàm: ```assembly= ; void addBigNum() ; Calculate the sum of two big numbers ; First para: esi -> num1 ; Second para: edi -> num2 ; Third para: reversedNum1 ; Fourth para: reversedNum2 ; Store result in num1: [esp + 0x4] in the end ; Stack in the end before return will look like that ; [len] -> esp ; [edi] ; [esi] ; [edx] ; [ecx] ; [ebx] ; [eax] ; [old ebp] ; [return address] ; [reversedNum2] ; [reversedNum1] ; [num2] ; [num1] -> it stores the result in the end addBigNum: ; Backup things push ebp mov ebp, esp push eax push ebx push ecx push edx mov esi, [ebp+0x14] ; num1 mov edi, [ebp+0x10] ; num2 push esi ; store num1 push edi ; store num2 call sLen mov eax, esi ; strlen(num1) mov esi, edi call sLen mov edx, esi ; strlen(num2) push eax ; Suppose it is the max length cmp eax, edx ; Compare to find the max length jg reverseStep_addBigNum pop eax push edx ; len = strlen(num2) reverseStep_addBigNum: mov esi, [esp+0x4] ; num2 mov edi, [ebp+0x8] ; reversedNum2 call reverseStr ; if num2 = 30, after that it would be 03 mov esi, [esp+0x8] ; num1 mov edi, [ebp+0xc] ; reversedNum1 call reverseStr ; appendZeros_addBigNum: mov bl, byte "0" cmp eax, edx jl appendZeros_addBigNum_1 jmp appendZeros_addBigNum_2 ; if num2 is "03", append zeros to the right until it gets the max length appendZeros_addBigNum_1: mov [edi + eax], ebx ; edi is holding reversedNum1 so no need to change inc eax cmp eax, [esp] jl appendZeros_addBigNum_1 jmp finished_appending_addBigNum appendZeros_addBigNum_2: mov edi, [ebp+0x8] ; change to reversedNum2 mov [edi + edx], ebx inc edx cmp edx, [esp] jl appendZeros_addBigNum_2 jmp finished_appending_addBigNum finished_appending_addBigNum: mov esi, [ebp+0xc] ; make esi -> reversedNum1 mov edi, [ebp+0x8] ; make edi -> reversedNum2 ; this is preparation for calculating and storing result in reversedNum1 xor eax, eax ; store digit mov bl, 10 ; dividing xor ecx, ecx ; counter: from 0 -> max length stored at [esp] xor edx, edx ; the carry addingLoop_addBigNum: push edx ; backup carry xor eax, eax ; cleanning up xor edx, edx ; cleanning up mov al, [esi + ecx] mov dl, [edi + ecx] sub al, "0" ; num1[i] - "0" sub dl, "0" ; num2[i] - "0" add al, dl ; num1[i] - "0" + num2[i] - "0" pop edx ; restore carry add al, dl ; digit = num1[i] - "0" + num2[i] - "0" + carry div bl ; ah = ax % bl ; al = ax / bl -> carry add ah, "0" ; (digit % 10) + "0" mov [esi + ecx], byte ah; num1[i] = (digit % 10) + "0" mov dl, al ; carry = digit / 10 inc ecx ; i++ cmp ecx, [esp] ; cmp i, max length jne addingLoop_addBigNum cmp edx, 1 ; if there is a carry after adding jne finished_addBigNum add dl, "0" ; append the carry to the end of reversed string mov [esi + ecx], byte dl finished_addBigNum: add esp, 0x4 ; skip the max length value mov esi, [esp + 0x4*9] ; point to reversedNum1 mov edi, [esp + 0x4*11] ; point to num1 call reverseStr ; Restore things pop edi pop esi pop edx pop ecx pop ebx pop eax mov esp, ebp pop ebp ret ``` và code tính tổng hai số big num sẽ như sau: ```assembly= global _start %include "functions.asm" section .bss num1: resb 100 num2: resb 100 reversedNum1: resb 100 reversedNum2: resb 100 section .data msg1: db "Input the first number: ", 0 msg2: db "Input the second number: ", 0 msg3: db "The sum of two numbers is: ", 0 section .text _start: lea esi, [msg1] call sPrint ; Print msg1 lea esi, [num1] call sScan ; Input num1 lea esi, [msg2] call sPrint ; Print msg2 lea esi, [num2] call sScan ; Input num2 push num1 ; ebp+0x14 push num2 ; ebp+0x10 push reversedNum1 ; ebp+0xc push reversedNum2 ; ebp+0x8 call addBigNum ; addBigNum(reversedNum2, reversedNum1, num2, num1) lea esi, [msg3] call sPrint ; Print msg3 lea esi, [num1] call sPrint ; Print result call quit ``` - Note một chút lí do vì sao mình lại sử dụng `reversedNum1` và `reversedNum2` ở kia. Khác với các code handle Big Num khác (như [code của anh Sơn Doraemon](https://github.com/SonVH2511/trainingKCSC/blob/main/TASK1_FIB%2CSORT%2CBIGNUM/descript.md) sử dụng stack để lưu từng digit) thì mình sử dụng hẳn hai vùng nhớ để lưu chuỗi đảo ngược của `num1` và `num2`. Với mình điều này khá tối ưu, tránh được khả năng bị overflow với số quá lớn. Tuy nhiên điểm bất cập là phải cung cấp địa chỉ của hai vùng nhớ `reversedNum1` và `reversedNum2` không liên quan gì đến hàm tính tổng, gây khó hiểu cho người đọc. - Sau khi done thì mình đã cố sử dụng stack để lưu chuỗi thay cho `reversedNum` nhưng quên mất một điều là stack chỉ có 4 byte, tức lưu được đến 4 chữ số là max, khi đó nếu tiếp tục chạy sẽ bị tràn, nói cách khác là buffer overflow. Mình đã khá ngu khi cố ngồi fix để không phải truyền `reveredNum` mà không nhận ra vấn đề. Trong tương lai nếu có thời gian mình sẽ quay lại và fix nó để tối ưu hơn, còn bây giờ tạm thời code như vậy. ![addBigNum](https://hackmd.io/_uploads/SyQYdTjJJx.png) - Mình để max là 100 chữ số nên chỉ dám test big num đâu đó tầm 9x chữ, nếu hơn thì khả năng xảy ra bug rất nhiều :crying_cat_face: - Lỗi đây: ![99digits](https://hackmd.io/_uploads/SJsEYps1yx.png) - 98 chữ số thì bình thường: ![98digits](https://hackmd.io/_uploads/HyLcFpiyyx.png) - Vậy nên muốn tăng max digits lên thì chỉ cần tăng size của .bss lên là được, ở đây mình set 100 cho đẹp :smile_cat: > Bài viết sau là về tìm số fibonacci thứ n, mình sẽ viết sau khi ôn xong môn "Tư tưởng Hồ Chí Minh", còn giờ mình phải cấp tốc học. Hi vọng bài viết này hữu ích với các bạn. Dear!!! ## 4. Updating - Mình đã upload file `functions.asm` mới ở [đây](https://github.com/Zupp30/Zupp_Learn_Reverse/blob/c76a93eb137602f9df46a38d0a2b790376eb60e2/functions.asm) ### New function - Mình đã comeback sau khi thi, và tìm ra một phương án tối ưu, hợp tình hợp lí hơn. Ý tưởng của mình vẫn như trước, chỉ có điều hàm sum giờ nhận vào 3 tham số, lần lượt là `(sum, num2, num1)` với `sum` là nơi lưu tổng. - Code: ```assembly= ; int sumBigNum() ; Calculate the sum of two big numbers ; First para: sum -> destination to store ; Next paras: num2, num1 ; After finished label, num1 holds result reversed, sum holds the right one sumBigNum: ; Backup things push ebp mov ebp, esp push eax push ebx push ecx push edx ; Code ; First to reverse the two string ; sum = reversedStr(num1) ; num1 = reversedStr(num2) mov esi, [ebp+0x10] ; num1 mov edi, [ebp+0x8] ; sum call reverseStr mov esi, edi call sLen mov edx, esi push edx ; suppose it is max_len mov esi, [ebp+0xc] ; num2 mov edi, [ebp+0x10] ; num1 call reverseStr mov esi, edi call sLen mov eax, esi cmp eax, edx jl pre_appendZeros pop edx push eax ; change max_len if eax > edx pre_appendZeros: mov esi, [ebp+0x10] ; point to num1 mov edi, [ebp+0x8] ; point to sum mov bl, byte "0" ; prepare byte to append cmp eax, [esp] ; compare to add "0" jl appendZeros_num1 jmp appendZeros_sum appendZeros_num1: mov [esi + eax], ebx inc eax cmp eax, [esp] jl appendZeros_num1 jmp pre_addLoop appendZeros_sum: mov [edi + edx], ebx inc edx cmp edx, [esp] jl appendZeros_sum jmp pre_addLoop pre_addLoop: ; this is preparation for calculating and storing result in num1 xor eax, eax ; store digit mov bl, 10 ; dividing xor ecx, ecx ; counter: from 0 -> max length stored at [esp] xor edx, edx ; the carry addLoop: push edx xor eax, eax xor edx, edx mov al, [esi + ecx] mov dl, [edi + ecx] sub al, "0" ; num1[i] - "0" sub dl, "0" ; sum[i] - "0" add al, dl ; num1[i] - "0" + sum[i] - "0" pop edx ; restore carry add al, dl ; digit = num1[i] - "0" + sum[i] - "0" + carry div bl ; ah = ax % bl ; al = ax / bl -> carry add ah, "0" ; (digit % 10) + "0" mov [esi + ecx], byte ah; num1[i] = (digit % 10) + "0" mov dl, al ; carry = digit / 10 inc ecx ; i++ cmp ecx, [esp] ; cmp i, max_len jne addLoop cmp edx, 1 ; if there is a carry after adding jne finished add dl, "0" ; append the carry to the end of reversed string mov [esi + ecx], byte dl finished: add esp, 0x4 ; skip the max_len ; mov esi, [ebp + 0x10] ; num1 ; mov edi, [ebp + 0x8] ; sum ; but no need to do that call reverseStr ; sum = reversed(num1) pop edx pop ecx pop ebx pop eax mov esp, ebp pop ebp ret ``` - Với hàm mới này, mình không cần phải truyền vào `reversedNum` nữa. Thay vì lưu chuỗi đảo ngược của `num1` và `num2` trong `reversedNum1` và `reversedNum2`, mình thay đổi nó thành `sum` và `num1`. Khi đó, mình sẽ lưu chuỗi kết quả ngược vào `num1`, kết quả xuôi vào `sum`, cuối cùng đạt được hiệu quả tương tự hàm cũ nhưng tối ưu và đẹp hơn nhiều. - Ví dụ sử dụng hàm: ```assembly= push num1 ; ebp+0x10 push num2 ; ebp+0xc push sum ; ebp+0x8 call sumBigNum ; sumBigNum(sum, num2, num1) ```