# Computer structure ## CPU - CPU giống như bộ não của máy tính, nó sẽ tính toán và quản lý các dữ liệu cùng với các instruction đc đưa ra - CPU là viết tắt central processing unit - CPU còn bao gồm các bộ phần nhỏ là ALU, CU và register - ALU là arithmetic and logic unit: phục vụ cho việc tính toán dựa vào các instruction,là nơi tính toán các thuật toán và logic - CU là control unit: là nơi điều khiển các lệnh thực thi nếu hợp lệ ### register - Cpu gồm có register là phần load và process data nhanh nhất trong máy tính, tuy nhiên nó cũng là bộ phận lưu trữ ít bộ nhớ nhất và cx có thời gian lưu trữ ngắn hạn - register cũng rất đắt tiền và nhỏ, vì vậy mỗi máy tính chỉ chứa vài chục register. - bình thường máy tính sẽ có cỡ 10 - 20 register đc sử dụng for general purpose, vd như viết code assembly - - CPU thường sử dụng register để hoạt động nhanh nhất có thể, nhưng vì register chứa quá ít dữ liệu và có thời gian lưu trữ ngắn hạn nên ta có thêm RAM và ổ đĩa ### RAM - RAM là phần chứa các data cần thiết cần sử dụng hiện tại và có thời gian lưu trữ lâu hơn các register. Nhưng lại có tốc độ xử lý chậm hơn nhiều so với register => nói cách khác, 1 bộ phận có bộ nhớ có sức chứa càng lớn thì tốc độ xử lý càng chậm - Nếu tính cả RAM ảo và ram thật thì lượng ram 1 máy tính có thể có tới hàng terabytes ### Ổ đĩa - Nơi lưu trữ bộ nhớ lớn nhất là disk, có thể chứa tới hàng trăm gb - Đây cx là nơi xử lý dữ liệu chậm nhất nhưng cs thời gian lưu trữ dữ liệu lâu nhất trong các bộ nhớ - Bình thường khi CPU xử lý data thì các data cần thiết sẽ được cấp qua RAM trc rồi mới tới register, điều này giúp cho việc load và process dữ liệu tối ưu và nhanh hơn so với việc lấy trực tiếp từ disk => Do đó disk đc coi như nơi lưu trữ dữ liệu lâu dài, RAM là nơi lưu dữ liệu cần thiết hiện tại, và register là nơi đc load và process data liên tục nhờ vào các instruction nhận đc ## Assembly - Ngôn ngữ assembly là ngôn ngữ level cực thấp mà CPU hầu như có thể đọc và hiểu trực tiếp được - Lý do vì ngôn ngữ này làm việc trực tiếp với register và cả memory - Assembly cho phép mình truy cập vào địa chỉ bất kì trong memory hoặc các general register để load or save data mình muốn - CPU có thể đọc, hiểu và làm theo các instruction của 1 program assembly mình đã code thông qua các bước là: - Chuyển đổi file code thành object file với lệnh as -o filename.o filename - Tiếp theo là link các object file lại với nhau qua bước ld -o filename filename.o để có thể bước tới final execution - Mình bắt buộc phải link nhiều hoặc 1 object file với cú pháp ld thì CPU mới đọc hiểu file cuối cùng đc ## Memory layout - Do RAM có cả ram ảo và physical ram, nên trong assembly, mình có thể truy cập vô 1 address bất kì. Tuy nhiên, nếu access vô vùng ko hợp lệ thì sẽ bị crash - Đối với cấu trúc x86-64 thì mỗi address có thể tiêu tốn tới 8 bytes, mỗi byte gồm 8 bit - Khi lưu dữ liệu vô memory: - ![image](https://hackmd.io/_uploads/H1drYLonel.png) - Cấu trúc của dữ liệu 8 bytes trong memory thường được chia làm 4 phần - Như trong hình thì mình có 0xc001c475 - với mỗi 2 ký tự tính từ 0x trở lên thì sẽ được cho là 1 byte - ví dụ như c0 thì được tính là byte đầu tiên trong hình - Trong memory còn có khái niệm MSB và LSB - least significant bit/byte: - LSB thường nhằm ở vị trí byte / bit cuối tính từ phải sang trái - Nó được coi là phần ít quan trọng nhất vì mỗi khi thay đổi 1 bit hay byte thì dữ liệu sẽ thay đổi theo từng đơn vị thay hàng chục - VD: 0x00000001 -> 0x00000002 -> thì dữ liệu sẽ đổi từ con số 1 sang 2 - Most significant bit/byte: - Đây là phần quan trọng nhất trong data vì mỗi khi thay đổi con số ở phần này có thể khiến dữ liệu lệch đi nhiều lần so với data cũ - Vì thế khi thay đổi phần này phải tính toán kĩ - Với mỗi 2 ký tự là 1 byte, trong 2 ký tự đó sẽ đc phân tích thành dữ liệu hệ binary giúp cho CPU đọc và hiểu được. Do đó trong hình mới gồm các con số 0/1 đối với mỗi byte - Và vì 1 byte là 8 bits nên mỗi byte sẽ đc phân tích thành 8 con số 0/1 - Nếu data chiếm hơn 1 byte thì dữ liệu sẽ đc ghi ngược: - vd: save 0x1201 - thì dữ liệu sẽ được lưu ngược lại là [01] [12] với mỗi [] là 1 byte - Đây được gọi là little-endian - little-endian thường được sử dụng trong memory: - 1 vd khác là 0xc001c475 - khi lưu vô memory thì sẽ được đảo ngược lại [75] [c4] [01] [c0] - Lý do là vì đôi khi có những lúc mình chỉ cần truy cập vô vài bytes data cuối trong memory, việc đảo ngược này khiến cho việc truy cập diễn ra nhanh hơn vì các bytes cuối đã được sắp xếp lên đầu - Còn 1 lý do nữa là vì các bytes cuối là LSB nên việc thay đổi ko ảnh hưởng quá nhiều đến data, vì vậy nên người ta ưu tiên thay đổi bytes cuối hơn các bytes đầu -> thuận tiện hơn khi đảo cách sắp xếp dữ liệu trong memory - Còn 1 điều liên quan nữa là tuy dữ liệu đc sắp xếp ngược nhưng address của data vẫn tăng lên dựa vào độ lớn của data ấy: - VD: 0xc001c475 được lưu ở address 0x4000 - Sau khi lưu trong thì các data hoặc instruction tiếp theo được lưu ở address 0x4008 - Điều này diễn ra là vì data trên chiếm tới 8 bytes data, vì vậy sau khi lưu ở address 0x4000 thì address cho next data/instruction sẽ là 0x4008 - Nếu 1 data/ instruc chiếm cỡ 2 bytes thì next address sẽ là 0x4002 => Do đó phép tính để tính các address tiếp theo sẽ là: $$ address2 = address1 + bytes $$ #### Stack ![image](https://hackmd.io/_uploads/SyWQb_s2el.png) - Memory còn chứa cả stack, stack được sử dụng như bộ nhớ tạm thời giúp cho mình có thể thay đổi data trong các register mà ko phải lo ngại mất data cũ - Điều này cho phép mình có thể thực hiện 1 số phép chuyển đổi dữ liệu giữa 2 register/memory - Mik còn có thể sử dụng stack như backup tạm thời, tránh bị mất dữ liệu cũ cần sử dụng lại - Nhờ cơ chế này, stack cx được sử dụng phổ biến trong các lời gọi hàm. Vì khi gọi hàm thì program sẽ nhảy tới address bất kì của hàm, khi kết thúc function. CPU sẽ lấy address đã được lưu trước đó và nhảy về đó, tiếp tục program progress - Cơ chế hoạt động của stack là Last in first out (LIFO): - Cơ chế này nghĩa là các data đã được lưu trước đó sẽ được lấy ra sau các data đã được lưu sau cùng - Vd như mình đặt 3 cuốn sách có tên 1, 2, 3 xếp chồng lên nhau. Cuốn 1 đặt trước, rồi tới cuốn 2 và 3 - Khi lấy ra thì cuốn 3 sẽ được lấy đầu tiên -> 2 -> 1. Đó cx là cách hoạt động của stack - Stack có cách ghi address ngược lại so với memory: - Vd: khi mình lưu data vào stack thì stack sẽ có 1 địa chỉ bất kì trong virtual memory, cho rằng address đó là 0x368 - Khi lưu data đầu tiên có độ lớn 8 bytes thì stack sẽ lùi về address 0x360 - Nếu data đầu có 4 bytes thì lùi về addr 0x364 => Do đó address stack sẽ lùi về con số tương ứng với số bytes mà data đó chiếm - Cách lưu data trong address cx áp dụng memory-endian. - Lưu ý là memory-endian chỉ áp dụng với data được lưu trong memory cũng như trong stack, vì stack cx là 1 phần của virtual memory mà OS cung cấp cho mình. - endianness ko áp dụng với register - Có 1 register tên là "rsp", mik hỉu là viết tắt register stack pointer. Đây là register trỏ về địa chỉ của stack đã được đặt sẵn trong virtual memory - Cách hoạt động của register này: ```assembly mov rax, 0x36 push rax ``` - giá trị của rax sẽ đc copy và lưu trên stack - Đối với x86-64 architecture, mỗi data được lưu trên stack sẽ tốn 8 bytes bất kể data đó có độ lớn dưới 8 bytes ```assembly 0x0FF8: 36 0x0FF9: 00 0x0FFA: 00 0x0FFB: 00 0x0FFC: 00 0x0FFD: 00 0x0FFE: 00 0x0FFF: 00 ``` - Do đó, cho rằng trường hợp này stack đc lưu trên địa chỉ 0x0FF8 đồng thời cũng là giá trị của register "rsp" - Khi lưu vô stack thì $$ rsp2 = rsp1 - 8 $$ - rsp luôn lưu địa chỉ đầu tiên của stack tính từ trái sang phải theo như hình này - ![image](https://hackmd.io/_uploads/SyWQb_s2el.png) - Để lưu hoặc load giá trị từ stack, mik có thể xài instruction push or pop: - Push: - Là chỉ thị cho cpu lưu 1 giá trị được xác định vào stack ```assembly mov rax, 60 push rax ``` - Lệnh này khiến cpu lưu giá trị 60 được lưu trữ trong rax vào stack - push cũng hỗ trợ lưu trực tiếp giá trị bất kỳ vào stack ```assembly push 60 ``` - pop: - Là chỉ thị load giá trị được lưu trên stack vào 1 register bất kỳ - Khác với push, pop chỉ hỗ trợ đối với register - Giả sử stack đang lưu [0x02] [0x01] ```assembly pop rax ``` - lệnh trên sẽ lưu giá trị 0x02 vào rax vì 0x02 được nhét vô stack sau cùng theo đúng LIFO - lúc này, rsi sẽ được + 8 bytes để đi tới địa chỉ cuối cùng mà value 0x01 chiếm bên trái - còn giá trị 0x02 trước đó vẫn sẽ được giữ nguyên, nó ko bị thay đổi nhưng stack cx ko đụng tới vì rsi hiện tại đang trỏ về địa chỉ cuối của value 0x01 - Do đó, khi push value mới thì value 0x02 sẽ bị đè khi rsi quay lại địa chỉ trước khi bị thay đổi. - Dựa vào cấu trúc máy tính mà mỗi value được lưu vô stack có thể chiếm số bytes khác nhau - x86-64 thì mặc định 8 bytes - x86 thì mặc định 4 bytes #### Memory - Mỗi memory address sẽ chiếm 1 byte - nếu value được lưu vào register lớn hơn 1 byte, vd 2 bytes: - Lúc đó, value này sẽ chiếm tới 2 memory address - VD: 0x2015 - address đầu tiên sẽ có value là 15 - address thứ 2 sẽ có value là 20 - thứ tự của value sẽ được đảo ngược theo little-endian - Trong memory, còn có 1 số tên gọi cho độ lớn của value là word, dword, qword và byte - Trong modern architecture, 1 word = 8 bytes = 62 bit - Còn được định nghĩa là natural unit mà máy tính có thể compute đc trong 1 lần - Nhưng vì historical reason, lúc viết assembly thì: - 1 word = 2 bytes - dword là viết tắt của double word = 4 bytes = 32 bits - qword viết tắt cho quad word = 8 bytes - 1 bytes = 1 bit - Nhờ những thuật ngữ này mà khi viết code assembly, mik có thể lưu trực tiếp 1 giá trị bất kì vào 1 mem address ```assembly mov qword ptr [0x02], 0x53 ``` - Tuy nhiên cách nạp dữ liệu trực tiếp này sẽ bị giới hạn ở 1 số trường hợp ko hợp lệ, nên mik nên tránh làm v - Thay vào đó mình có thể xài 1 số cách dễ dàng hơn như ```assembly mov rax, 36 mov [0x02], rax ``` - Code này lưu thẳng dữ liệu trong rax vào bộ nhớ, ko cần phải xác nhận độ lớn của dữ liệu cần lưu - Đối với các general register, mỗi register hiện tại đều có sức chứa tới 64 bit. - Mik có thể truy cập vào các dữ liệu con của register đó bằng cách đổi vài ký tự trong tên gọi của register ```assembly rax: 8 bytes eax: 4 bytes ax: 2 bytes ah: higher 8 bits al: lower 8 bits ``` - Quy luật thay đổi của 1 số general register mà mình nhìn thấy là: - đổi chữ cái đầu tiên thành e: rax -> eax, rdi -> edi = 4 bytes - bỏ chữ đầu tiên: rax-> ax, rdi -> di, rbx -> bx = 2 bytes - Bỏ ký tự đầu và thay thế ký tự cuối: 1 byte - rax -> al, ah - rdi -> dil - rsi -> sil - rbx -> bl, bh - đối với 2 byte cuối, syntax có thể khác so với quy luật mà mình tự nhìn ra. - Việc truy cập vào các register con của các register giúp mình mở rộng khả năng sử dụng dữ liệu ```assembly= mov [0x01], eax or mov dword ptr [0x01], rax ``` - 1 trong 2 lệnh trên đều lưu giá trị 32 bit của rax - Thay vì lưu hết giá trị 64 bit của rax thì việc lưu từng phần này giúp tối ưu bộ nhớ đc sử dụng hơn ##### Assembly - Assembly cho phép mình load và xử lý logic các data từ general register và cả memory - Mình có thể lưu 1 dữ liệu mình muốn vào 1 register có tên rax: ```assembly= mov rax,60 ``` - lệnh mov là lệnh copy thẳng con số 60 vô rax. - - Lưu ý là mov là viết tắt move, nhưng bản chất nó lại là lệnh set. 1 lệnh ghi đè giá trị mới lên giá trị cũ - Mình có thể lưu dữ liệu của 1 register a vô register b ```assembly= mov rax, rbx ``` - rax sẽ copy data của rbx và chèn lên data cũ của rax (nếu có). Còn rbx thì vẫn giữ nguyên data ```assmbly= mov [address], rax ``` - Assembly cho phép mình làm việc với memory qua address và register - Lệnh trên sẽ lưu data của rax vào vị trí đc xác định - [address] có thể coi như là 1 con trỏ chỉ thẳng về phía địa chỉ có tên address, tại đó assembly sẽ tiếp tục đưa ra instruction làm việc với value tại địa chỉ đó - Trong trường hợp trên là copy rax data lên data tại address. - Nó như là địa chỉ 1 căn nhà, khi tới nhà thì mik sẽ làm việc với chủ nhà. - Và điều ngược lại có thể diễn ra ```assembly! mov rax, [address] ``` - Đây là instruction copy data tại vị trí address lên register rax - VD: address= 0x02 và chứa value là 0x05 - lúc này cpu sẽ đưa ra instruction copy value 0x05 tại địa chỉ 0x02 lên rax - Từ đó, ta thấy rằng nếu có data x đc đặt trong [] thì nó sẽ được coi như là con trỏ, trỏ tới vị trí x đó. - Nên ta còn có cách xài register nữa là: ```asssembly! mov rax, 60 mov [rax], rbx ``` - Lệnh trên sẽ lưu data trong rbx vô địa chỉ là rax, quy đổi thì rbx sẽ lưu vô địa chỉ 60 ##### System calls - System calls là những instructions gọi đến hệ điều hành của mình, thực hiện các lệnh dựa vào hệ điều hành - Để gọi system calls: ```assembly= mov rax, number syscall ``` - Ta có thể gọi system call bằng cách xác định số thứ tự và loại system call đó -> lưu số thứ tự vô rax r xài lệnh syscall - Có tới hơn 300 lệnh syscall trong linux phục vụ cho nhiều mục đích khác nhau - 1 syscall thường dùng để chương trình ko crash là exit: ```assembly= mov rax, 60 syscall ``` - Mã lệnh syscall của exit là 60 - Và các syscall còn có các argument như các hàm bình thường - Đối với syscall exit thì chỉ có duy nhất 1 argument ```assembly= mov rdi, 42 mov rax, 60 syscall ``` - rdi thường là argument đầu tiên của syscall, kế tiếp là rsi, và thứ 3 là rdx - Đối với lệnh trên thì return value của exit sẽ là 42: exit(42) - Assembly cho phép sử dụng syscall read và write - Nó hoạt động như nguyên lý 0, 1, 2 của các program: - 0: input - 1: output - 2: error - Với read syscall: ```Assembly= .intel_syntax noprefix .global _start _start: sub rsp, 10 mov rdi, 0 mov rsi, rsp mov rdx, 10 mov rax, 0 syscall mov rdi, 1 mov rsi, rsp mov rdx, 10 mov rax, 1 syscall add rsp, 10 ``` - Dòng 1 cho CPU bt rằng code sẽ ko có prefix nào hết - Dòng 2 để thiết lập start -> Cho CPU bt nên bắt đầu đọc instruction từ đâu. Nếu ko có thì khi chạy program sẽ có thông báo ko bt chạy ở đâu, và mặc định chạy từ đầu code - Dòng 3 cho bt nơi khởi đầu mà CPU bắt đầu đọc - Đối với read/write syscall, 2 hàm này có 3 argument - Vì vậy read/wrote sử dụng cả 1. rdi, 2. rsi và 3. rdx - rdi: register này có vai trò cho biết file description, là loại output, input hay stderror - rsi: là argument và cx là reg thứ 2. Register này cho biết nên lưu read ở đâu và write các dữ liệu ở đâu. - rdx: là argument cho CPU biết nên đọc hay viết bao nhiêu bytes data - Trong trường hợp này, mik sử dụng stack như buffer. Mik sẽ trừ rsp đi đúng với số bytes mà mik cấp cho write/read. Do đó khi sử dụng 2 hàm trên thì CPU sẽ bắt đầu đọc từ rsp sau khi trừ cho tới rsp trước khi trừ là kết thúc. - Buffer được ví như nơi cấp phát storage tạm thời cho I/O - Có nhiều cách để tạo buffer trong trg hợp này. Nhưng mik chọn stack cho gần gũi - Nhược điểm PP stack là khi xài xong thì phải trả rsp về lại như cũ. Nếu ko thì các value kế tiếp sẽ tiếp tục trừ rsp về các địa chỉ nhỏ hơn, có thể làm crash vì lượng storage được cấp phát cho stack nhỏ hơn so với việc cấp phát động - Còn 1 điểm yếu khác là nếu cấp phát lượng bytes vượt quá mức mà rsp bị trừ thì có thể gây crash ##### Arithmetic - Assembly hỗ trợ tính toán logic với các register - ![image](https://hackmd.io/_uploads/SyY62Wh3xg.png) - Vd: add rax, rbx: - Lúc này phép tính là rax = rax + rbx - Lưu vô rax thành rax mới - Những cách có thể thực hiện các phép tính: ```assembly= mov rax, 5 ; rax +=5 mov rax, rbx ; rax +=rbx ``` - Lưu ý rằng ko thể trực tiếp sử dụng các phép tính đối với các pointer, vì vậy cần phải lưu value vô các reg trung lập để có thể thay đổi value ở địa chỉ mà pointer trỏ tới ```assembly= mov rax, [rdi] add rax, 5 mov [rdi], rax ``` ##### So sánh bit logic - Assembly hỗ trợ so sánh bit logic với các lệnh xor, or, and. - xor: lệnh này cho phép so sánh giữa 2 bit của 2 giá trị khác nhau - VD: 0x001 và 0x011 - Nó sẽ so sánh lần lượt 0 - 0, 0 - 1 và 1 - 1 - Các con số bit giống nhau: 0 - 0, 1 - 1 sẽ cho ra kết quả so sánh là 0 - 1 - 1 -> 0 - 0 - 0 -> 0 - 1 - 0 -> 1 - or: Lệnh này tương đương với xor, chỉ khác ở chỗ là 1 - 1 vẫn ra 1 - 1 - 1 -> 1 - 0 - 1 -> 1 - 0 - 0 -> 0 - and: Lệnh này chỉ cho ra 1 nếu cả 2 bit đều = 1 - 1 - 1 -> 1 - Còn lại -> 0 - Kết quả sẽ được lưu tại register đứng đầu như thường lệ ``` mov rax, 0x101 mov rdi, 0x011 xor rdi, rax -> rdi = 0x110 ``` ##### shl, shr - shl: shift left, là lệnh di chuyển các bit đúng n lần sang trái - VD: 0x00101 ```assembly= mov rax, 0x00101 shl rax, 2 -> rax = 0x10100 ``` - Tương tự với shl là shr: shift right - Lưu ý là nếu bit 1 mà di chuyển vượt tầm hiển thị của 0x thì nó sẽ bị biến thành 0 - VD: ```assembly= mov rax, 0x00110101 shl rax, 5 -> rax = 0x10100000 ``` - Mik có thể sử dụng cách này để cô lập data mà mik mong muốn - VD: mik muốn cô lập 0x00110101 thành 0x00000101 ```assembly= mov al, 0x00110101 shl al, 5 -> al = 0x10100000 shr al, 5 -> al = 0x00000101 - Lưu ý, Cách này áp dụng với từng byte, 8 bit thì mới cho ra kết quả trên. Nếu áp dụng với 2 bytes trở lên thì do các bit trong mỗi bytes đều di chuyển n bước nên con số sẽ khác nhau nhiều ##### Jump - Assembly hỗ trợ sử dụng lệnh conditional jump và unconditional jump - Jump là lệnh khiến program nhảy vọt tới địa chỉ mà mình muốn ![image](https://hackmd.io/_uploads/H1zReXphxx.png) ###### Unconditional jump - lệnh jmp là unconditional jump. Có thể áp dụng với cả memory address, register và sẽ được thực thi mọi lúc nếu như nó xuất hiện ```assembly= mov rax, 60 jmp rax or jmp 60 or jmp [rax] ``` - Lưu ý rằng nếu sử dụng register để jump thì CPU sẽ nhảy tới địa chỉ có giá tị = giá trị register - Như trong vd trên thì CPU nhảy tới địa chỉ 60 ###### Jump conditional - Jump conditional là những lệnh chỉ đc thực thi nếu đúng với điều kiện đề ra - Thường có ký hiệu là j.. - Viết tắt của nhiều ký tự như : - ja: jmp if above - je: jmp if eqal - ... - Conditional jump chỉ áp dụng với các địa chỉ hoặc label, ko sử dụng đc với các register ##### Phép +, -, *, /, modulo - Assembly cho phép mik thực hiện các phép tính thường với các lệnh ```assembly= add rax, 30 sub rax, 30 mul rax, 2 imul rax, 2 mov rbx, 2 div rbx ``` - add là phép + - sub là - - mul và imul là phép nhân. mul xài với unsigned, imul xài đc với cả signed - div khá đặc biệt ở chỗ sẽ có phép dư trong nó - cấu trúc div là: rdx(remainder):rax(số chia) / reg - Chỉ có thể xài div với register đc, div reg - Ko thể chia trực tiếp số liệu