Table of Contents
Static and dynamic reverse engineering of five historical malware samples from raw disassembly in IDA Pro, spanning four distinct execution environments: 16-bit real-mode boot sector code, 16-bit DOS COM infectors, a memory-resident network worm delivered as a UDP payload, and 32-bit Windows PE infectors. No source code was available for any sample; all behavior was recovered by annotating disassembly instruction by instruction, with two samples requiring dynamic unpacking in a sandboxed VM before static analysis was possible.
These samples span four decades of malware evolution, but the primitives are the same ones underlying modern exploitation and defense: how code hides from analysis, establishes persistence, hijacks control flow, and propagates. Reading them at the instruction level means understanding those mechanisms directly rather than through a tool's summary of them.
Techniques exercised across the set
Interrupt vector table manipulation and memory residency
Self-modifying code and mid-instruction jump obfuscation
SEH abuse for anti-debugging
Hardware-based VM detection (SLDT, CPUID)
PE header and section table parsing
Export table walking for import-free API resolution
Control-flow hijacking through call patching
Polymorphic code generation
Stack buffer overflow exploitation
Return-address hijacking through jmp esp
PRNG-driven network propagation
Tools
IDA Pro for static disassembly and annotation across all five samples, working in 16-bit real mode, 16-bit DOS, and 32-bit PE segments. OllyDbg for dynamic analysis of Lucius and Harulf, both of which required stepping through their decryptor stubs to recover plaintext bodies before static analysis was possible. Sandboxed VM for detonation, which for Harulf meant working around anti-VM checks that would otherwise cause it to exit silently.
A boot-sector virus that installs itself into the interrupt vector table and spreads to every floppy disk touched by the infected machine.
Residency mechanism. The virus survives by stealing memory from the system before DOS loads. It reads the BIOS memory-size word at 0040:0013, decrements it twice to shrink the reported total by 2 KB, and computes the segment address of the newly orphaned region. The system never learns that memory exists, so nothing overwrites the virus.
INT 13h hooking. Before relocating, it saves the original disk I/O handler by reading the offset at 0000:004C and the segment at 0000:004E into its own body, then overwrites the IVT entry to point at ES:000E in the hidden 2 KB block. Every subsequent disk operation on the machine routes through the virus first. The relocation itself ends with a jmp dword ptr cs:byte_7C03 that transfers control into the copy in stolen memory.
Infection path. On each INT 13h call, the hook checks whether the request targets drive A. If so, it tests the floppy motor bit at 0040:003F: a running motor means the virus was just loaded from that disk, so no infection is needed; a stopped motor means a newly inserted disk. It then reads the target's MBR, compares the first two words against its own body to detect prior infection, and if clean, copies the disk's real 33-byte partition table into its own image before writing itself to track 0, sector 1. On hard drives it first relocates the original MBR to sector 7, head 0. Calls to other drives fall through to a direct jmp at the saved original handler address.
Payload. On March 6, the virus enters a loop writing garbage from an arbitrary memory segment across the disk, incrementing head and cylinder until the drive is destroyed.
A COM file infector notable for an anti-debugging technique built on interrupt redirection and self-modifying code.
The INT 3 / INT 21h swap. The virus copies the INT 21h handler's offset and segment from 0000:0084 into the INT 3 vector at 0000:000C. INT 3 is the breakpoint interrupt used by debuggers. From that point forward, every DOS service call the virus makes is issued as int 3 rather than int 21h, which means the virus's own system calls are indistinguishable from breakpoints, and a debugger attempting to trap breakpoints intercepts the virus's normal operation instead.
Divide-by-zero dispatch. The virus saves the original INT 0 (divide error) handler, then points INT 0 at one of two addresses depending on execution context. Line seg000:0100 self-modifies the operand at seg000:0107 from 168h to 152h, and a matching instruction later writes whichever value is live into the INT 0 vector. Outside a debugger the sequence resolves to 168h, the legitimate continuation path. Under a debugger the timing differs and 152h is written instead, which lands in a routine that enters an infinite int 13h loop overwriting every sector of the first floppy drive with the virus body. After the check passes, the virus rewrites seg000:0107 back to 168h so the modification leaves no trace.
Mid-instruction jump. At seg000:0157 the byte B8 is placed such that a linear disassembler decodes it as mov ax, 0FE05h, but execution actually jumps into the middle of the instruction stream, where the bytes decode as add ax, 0EBFEh; cld; and realign at seg000:015C. Subsequent arithmetic (sub ax, 0E702h) reconstructs 0301h — the INT 13h write-one-sector command — in a register rather than loading it as an immediate, so the operation never appears as a constant in the disassembly.
Infection. The virus searches for *?.C?M files, reads each candidate, and compares the first word against its own to skip already-infected hosts. It prepends itself to the file, copying only 419 bytes (1A3h) so that the signature region and original file content are preserved. If it finds COMMAND.COM specifically (identified by a second-word comparison against 6015h), it overwrites three strings in the host binary: the copyright banner, the "has no label" volume string, and the "Bad Command" error message, replacing them with taunt text including a claim that DOS 6 antivirus missed it.
SQL Slammer — memory-resident UDP network worm (Click to expand)
A 376-byte worm carried entirely in a single UDP packet to port 1434, requiring no file to be written to disk.
The vulnerability. The SQL Server Resolution Service dispatches on the first byte of the packet as an index into a jump table. Command code 04 routes to a handler that builds a registry path with sprintf into a 128-byte stack buffer, concatenating a 48-byte prefix, the attacker-supplied packet contents as a null-terminated string, and a 28-byte suffix. Any packet body over 52 bytes overflows the buffer and overwrites the saved return address.
Exploitation. The worm fills the overflow with 01 filler bytes, then places 0x42B0C9DC at the return address slot — the address of a jmp esp instruction inside sqlsort.dll. On return, execution transfers to jmp esp, which redirects to the next byte on the stack, a short jump into the worm body.
Stack reconstruction and self-relocation. Because the vulnerable function's epilogue popped the stack, the worm rebuilds its own frame: it pushes the jmp esp address back, loads 0x01010101 into eax, and pushes it 24 times via a loop, then XORs to produce 0x04000000 and pushes that. It sets ebp to the current stack pointer so the worm body sits above ebp (accessible via positive offsets) and its working stack below (negative offsets), making the whole payload position-independent.
API resolution. It calls LoadLibraryA (resolved from a fixed address in sqlsort.dll) on ws2_32.dll and kernel32.dll, keeping both module handles on the stack. It resolves GetProcAddress by reading the first four bytes at the candidate address and comparing against 0x51EC8B55 (the standard function prologue push ebp; mov ebp,esp; sub esp,...) to verify it found the right function, falling back to an alternate address if the check fails. It then resolves GetTickCount, socket, and sendto.
Propagation. GetTickCount seeds a linear congruential generator implemented entirely in lea and shl instructions: successive operations compute 3×, 13×, 208×, 209×, 53504×, 53503×, and finally 214013× the seed, then add a value derived from the GetProcAddress address XORed with a constant. The result is used directly as a target IPv4 address. It opens a UDP socket (AF_INET, SOCK_DGRAM, IPPROTO_UDP) and calls sendto in a tight loop with a 376-byte payload pointer at ebp+3, skipping the leading zeros of its own body, flooding randomly generated addresses on port 1434.
Position independence and encryption. The virus establishes its own load address with a call $+5 / pop ebp / sub ebp, 401005h sequence, then checks a key stored at [ebp+401061h] — a value of zero indicates the body is unencrypted (first generation), non-zero triggers the decryptor.
PEB-based kernel32 discovery. Rather than relying on imports, it walks fs:[eax+48] to reach the Process Environment Block, follows the PEB_LDR_DATA pointer at offset 12, reads the module list pointer at offset 28, and extracts the base address from the first LDR_MODULE entry. A sign check on the PEB pointer selects an alternate Win9x-compatible discovery path. From that base it parses the PE export table to resolve roughly 17 functions by name (LoadLibrary, FindFirstFile, CreateFileMapping, MapViewOfFile, mutex APIs, and others).
Recursive infection. Seeded by GetTickCount, it calls SetCurrentDirectory and recurses through the directory tree: for each entry it calls GetFileAttributes, skips names beginning with ., descends into directories (recursing, then SetCurrentDirectory("..") to return), and for files runs an extension check that builds a 4-byte value via lodsb/rol and compares against 2E657865h (".exe").
PE modification. For each target it opens the file, computes a new mapping size (original + 0xC31 virus body + 4 KB slack), and reads the PE headers. It checks OptionalHeader.CheckSum against 0xFFFF as an infection marker, unmapping and closing if already infected. It scans section headers to find the last section with initialized data, computes the insertion point at that section's PointerToRawData + VirtualSize, and enlarges both VirtualSize and SizeOfRawData to accommodate the body.
Control-flow hijacking via CALL patching. Rather than modifying the entry point, the virus locates the section containing the original entry point, scans it with repne scasb for the first 0xE8 byte (CALL rel32), and rewrites that instruction's relative displacement to point at the injected body. It saves the original displacement and appends a push <original_target>; ret stub after the virus code, so the hijacked call returns to its intended destination after the payload runs. The host appears to execute normally.
Encryption. The body is encrypted with a Lehmer-style PRNG (constants 0x41A7, 0x1F31D, 0xB14) generating a keystream XORed byte by byte into the target. Only the first 0x61 bytes — the decryptor stub and key — are written in plaintext.
Payload. After infection it loads user32, resolves MessageBoxA through the same export-parsing routine, displays a dialog captioned "Win32.Lucius" reading "...I am Lucius, Prince of Ruin...", then zeroes its working buffer.
The most heavily defended sample, combining SEH-based anti-debugging, hardware-level VM detection, two layers of encryption, and a runtime-assembled polymorphic decryptor.
SEH anti-debugging. The virus installs a custom EXCEPTION_REGISTRATION_RECORD by pushing its own return address as the handler and repointing fs:0 at the new frame, then deliberately dereferences a null pointer. Without a debugger, Windows walks the SEH chain and lands on the virus's handler, which jumps to the delta-offset routine. With a debugger attached, the debugger intercepts the exception first and the intended control flow never occurs.
Anti-VM checks. Two independent tests. The first executes SLDT and inspects the returned Local Descriptor Table Register value; a zero result indicates virtualization and the routine returns immediately. The second calls CPUID with eax=0 (returning the maximum supported leaf), pushes that, calls CPUID again with eax=1 (returning the version/signature), and XORs the two values — a match indicates a VM and the virus exits.
kernel32 discovery by backward scan. Instead of the PEB, Harulf takes the return address from the stack and scans backward through memory: at each candidate it reads the potential e_lfanew field, discards values with any of the top five bits set (too large for a valid PE header offset), verifies that the address at [edx+ecx+34h] matches the candidate base (confirming the ImageBase field), and checks for the MZ signature (5A4Dh). It then parses the export directory to locate GetProcAddress by string comparison, and from there resolves functions from kernel32.dll, advapi32.dll, and shell32.dll.
Two-layer encryption. The virus body (0x1720 bytes) is encrypted in two stages. An inner layer uses a rotate-based scheme: for each word, xchg ch, cl followed by rol ch, cl, applied across the body minus the 72-byte encryption loop itself. An outer layer XORs the entire body with a random single-byte key (1–255) generated per infection.
Polymorphic decryptor construction. Three functionally equivalent decryptor variants exist in the virus body, dispatched by selector values 00h, 1Dh, and 40h. They differ in register allocation and instruction selection: one uses pop edx and nop padding between every operation, another manipulates the stack with add esp, 4 and push [edx], and the third uses adc esi, 1 instead of inc esi and xchg ecx, ecx as an effective no-op. At infection time the virus randomly selects variants, measures each one's length with a bundled length-disassembler routine (VirXasm32), and assembles them into a zeroed buffer at the host's entry point — so no two infected files carry byte-identical decryptors.
Entry point patching. It writes a MOV EAX, <virus_data_VA XOR key> instruction at the host entry point, so even the address of the virus body in the infected file is obfuscated, then copies the assembled decryptor and body after it.
Storage via resources. Rather than only appending to a section, the virus stores its encrypted body as a resource: BeginUpdateResourceA, then UpdateResourceA with type RT_RCDATA and resource ID 0x4D2 (which doubles as an infection marker), then EndUpdateResourceA.
UAC bypass. It calls IsUserAnAdmin from shell32.dll; without admin rights it displays an error box and exits. With admin rights, it opens HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\System with KEY_SET_VALUE and sets EnableLUA to 0 via RegSetValueExA, disabling User Account Control on Windows Vista and later.
Propagation. Three vectors. It copies itself to eight hardcoded P2P shared folders (including C:\Program Files\Kazaa Lite\My Shared Folder\vista_crack.exe) under enticing filenames. It enumerates all logical drives with GetLogicalDrives, and for each present drive infects executables at the root, copies itself as harulf.exe, and writes an autorun.inf containing open=harulf.exe and shell\open\command=harulf.exe so the payload runs when the drive is opened. It also recursively infects .exe files in the working directory tree.
Date-triggered payload. It calls GetDateFormatA with a dd format string and compares the day of month against 09. On a match, it loads urlmon.dll, resolves URLDownloadToFileA, downloads a JPEG from a remote server to C:\saddamsfamily.jpg, sleeps three seconds, and opens the image with WinExec on explorer.exe.
Cryptographic hardware verification on an Intel DE10-Standard (Cyclone V SoC). Cores were provided; the work was integration logic, test design, and analysis.
SHA-256 hash checker — Wrote a comparator module, top-level integration, and a control FSM to run a provided SHA-256 core standalone on the board with switch-selected inputs and LED output. Verified in ModelSim against software-generated hashes, then closed timing in Quartus.
AES co-verification — Generated an original 50-case test suite (5 plaintexts × 5 keys, encryption and decryption) from a C reference, extended the VHDL testbenches to run it, and verified FPGA ciphertext against software output by driving an AES accelerator over the lightweight AXI bridge from embedded Linux on the ARM HPS.
SHA-3/Keccak analysis — Measured throughput across three Keccak core architectures in ModelSim, reparameterized the mid-range core across four block/round configurations, and synthesized each in Quartus to find maximum clock frequency and resource utilization, comparing throughput/area tradeoffs.
NIST SP 800-22 randomness testing — Ran the NIST statistical suite on π and on 640,000 bits of AES ciphertext at two significance levels, analyzing p-values and pass proportions to assess whether encryption makes a non-random input statistically indistinguishable from random.
Three semesters on the ECE subteam, taking a legacy KiCad design through a full Altium rebuild to a routing-ready board. Subsystems: USB hub, 5-channel receiver, BNO055 IMU, manual override, motor control, power.
Came up to speed on Altium through the education course (schematic capture, PCB conversion and routing, datasheet analysis) before taking on the migration.
Diagnosed the failed migration. Traced import errors to KiCad naming conventions broken by an update, fixed shield pad naming, set up the project GitHub. Re-import still failed, confirming the design needed a full rebuild. Rebuilt the manual override subsystem in Altium, sourcing custom footprints and replacements for out-of-production parts.
Built the top-level interface board schematic, integrating the receiver, manual override, M-subsystem, and BNO055 as hierarchical sheets.
Converted bus ports to individual nets across four subsystems after Altium's bus naming rules made the KiCad buses unusable, updating sheet symbols, port directions, and conflicting net labels.
Drove the schematic to a clean ERC, fixing receiver wiring errors, a D+ polarity error on IC1, missing designators, and a GND network fault that took several weeks to trace.
Optimized for board space: swapped Teensy 4.1 to 4.0, moved the receiver off-board behind a connector, replaced oversized connector and through-hole LED footprints, and removed the unused J5 connector.
Coordinated with the mechanical subteam on board dimensions and mounting holes; began integrating the motor controller onboard from SimpleFOC Mini documentation; divided the board into sections and began routing.
A memory-mapped FPU peripheral extending the SCOMP processor with IEEE-754 half-precision add, subtract, multiply, and divide, implemented in VHDL on an Intel DE10 FPGA. Team of 4; each member owned one arithmetic module, and we built the top-level wrapper together.
Chose half-precision floating point over fixed point, trading hardware simplicity for the precision and dynamic range needed for physics simulation (gravity, thrust, motion), after determining SCOMP's throughput wouldn't be the bottleneck.
Designed and verified the arithmetic units with normalization, rounding, and overflow/underflow handling across all four operations.
Designed a calculator-style API modeled on SCOMP's own accumulator interface, so the peripheral felt native to existing SCOMP developers. Chaining operations costs a single out call, since each result stays in the accumulator as the next operation's first operand — a substantial reduction in instructions per calculation.
Mapped six I/O registers (status, accumulator, and one per operation at 0x90–0x95), using the operation address itself to both load the second operand and trigger the computation.
Added a status register for multi-cycle operations, letting software poll to completion rather than assume fixed latency — necessary since divide takes considerably longer than add.
Built the top-level wrapper integrating the FP core with SCOMP's I/O bus (IO_ADDR, IO_DATA, IO_READ, IO_WRITE) and dispatching to the four arithmetic sub-modules.
Verified functionality in ModelSim with testbenches, then performed post-synthesis debug on the DE10 in Quartus using SignalTap.
Built a PyTorch system that predicts multi-label genres (18) and single-label moods (~25) from 30-s audio, trained on 50k+ tracks (~500 GB) with a 432-D feature vector (MFCCs, contrast, flatness, ZCR, chroma, tonnetz, tempogram, tempo).
Engineered cached librosa feature extraction; trained an attention-gated MLP with smoothed inverse-frequency class weighting, multilabel-stratified splits, per-class PR-curve F1 thresholding, and 10-seed probability ensembling.
Delivered genre macro-F1 ≈0.85–0.86 (micro ≈0.83) and mood top-1 ≈0.70 on frequent moods (≥500 examples), plus a CLI for single-file inference using saved scalers/encoders/thresholds.
Code that was used in developing the models can be found at https://github.com/Yusufff7/ai-music-categorizer-setup/
This is a portable retro game console emulator that uses a Raspberry Pi, IPS screen, and custom breadboard controller.
The video demonstrates the device's functionality as I use the custom controller to control Pokemon Emerald, a game originally designed for the Gameboy Advance console.
Above is a close-up image of the breadboard's circuitry as well as the wires connecting to the pins on the Raspberry Pi.
Above is an alternate angle of the final product as the game is running on the IPS screen.
The first step to completing this project was to install the retropie OS onto a micro SD card to then insert into the Raspberry Pi.
The model used here was a 3B.
A power supply UPS HAT was then attached to the Raspberry Pi and screwed into place.
This allows the system to run on 3.7 volt 18650 rechargeable batteries instead of having to be continuously plugged in.
The IPS screen was then connected to the power supply and Raspberry Pi using a micro USB cable and HDMI ribbon cable.
The initial OS setup was then run using a keyboard, including mapping the keyboard keys to controller controls.
The emulator cores were then installed by connecting the Pi to the internet and using Retropie OS's installing system.
To get the game files onto the Raspberry Pi, I had to SSH onto the Pi's file system and transfer the ROM files from my PC.
The controller was then built using a breadboard, tactile switches, and jumper wires.
The wires for ground, power, and each of the switches were then connected to the appropriate GPIO pins on the Raspberry Pi.
The Pi's terminal was then used to map the GPIO pins to their corresponding controller buttons.
After confirming the controller's functionality by testing it with the software, the jumper cables were trimmed and flattened out on the breadboard.
The top picture shows the controller before the wires were trimmed/flattened, and the bottom picture shows the controller afterward.
The portable handheld retro gaming console was then completed and ready for use.
This AI support bot was programmed using Python, utilizing both the Discord.py API as well as Google's Gemeni API.
The bot was created to automate the process of answering questions that any members of SiliconJackets post in our communication channels on the messaging platform Discord.
Note: SiliconJackets is an organization at Georgia Tech that conducts the chip production process in a way similar to semiconductor industries, all the way from ideation to tapeout.
The bot was trained using past message history in our communication channels so that it may answer any question that has been asked and answered by our members in the past as well as have solid background knowledge if any newer questions are to arise.
The full code is open-source and available here:
https://github.com/Yusufff7/SiliconJackets-AI-Support-Discord-Bot/
Above is a demonstration of this bot's functionality on the messaging platform.
Overview
I was commissioned to create UI/UX designs (in Figma) for a religious calendar app that would allow users to track their religious fasting progress as well as view holidays and their own events on a single calendar.
Home Screen
Schedule / Add Event Screens
Track Screen / Friends Screen
Overview
Aurality is a streaming service that I co-founded alongside 5 others for independent artists to upload and share music.
Unlike streaming services like Apple Music and Spotify, which pay small artists a very small wage due to the universal subscription model, Aurality operates on a subscription model in which users pay to subscribe to individual artists.
This subscription form is more analogous to a platform like Patreon or Twitch, where people who enjoy someone's work and would like to support them can access all of their content by paying them a small donation.
The website was developed in React and the cross-platform mobile app was developed in React Native (both used Typescript and Javascript). The backend is written in Node.js and uses a PostreSQL & Prisma database.
The beta was launched in August 2024 for a total of two weeks, in which we amassed over 300+ registered users, 1500+ uploaded tracks, and 7000+ visits.
The website and mobile app are currently in development for an official release with new features such as paid subscriptions to individual artists and a creator studio planned.
The majority of my contributions were with respect to the development of the creator studio, mobile app player, and UI/UX design.
This is the page in which users can create a compilation of songs and edit things like the tracks, titles, and album covers.
This is the create page after "Add Track" is selected, as the user is prompted with a modal that allows them to upload a file and edit the track's details.
Here is a view of what uploaded albums that are ready for users to play look like on site.
Creator Studio
Above is the Content page of the creator studio, which is finished and ready to be launched alongside the platform's official release.
All of a user's uploaded tracks along with their corresponding visibility, upload date, stream count, and features are listed.
When hovering over any individual track, the row becomes highlighted and three buttons (edit, share, and delete) become visible so that the user may interact with that track.
These are Figma designs that I created for the Analytics page, which is currently being developed to be implemented into the creator studio.
This page will allow artists to view the analytics of their content so that they can better understand how to cater to their audiences and grow their platform.
This page is planned to release alongside the Content page during the official launch.
Mobile App
This shows the mobile app running in an iPhone emulator on MacOS.
The left shows the album view in which users can select songs that they would like to play.
The right shows the song view modal, which appears after a user has clicked the floating song component above the tab navigator.
Overview
Along with a team of 8 other developers, I used React Native to develop a retail and streaming mobile application that was acquired by the Yeezy clothing brand.
When the mobile application was a startup before the acquisition, I was in charge of programming the entirety of the app's settings.
The images and videos below show all of the work I did prior to the acquisition, which I have been permitted to share.
All content shown has been screenshotted or screen-recorded on a working build of the app running on an iPhone through Expo Go.
Main Settings
This is the settings screen, which can be accessed directly from the app's home screen.
The video to the left demonstrates how the page was programmed to handle timezone and country selection.
Whenever a country is selected through the dropdown, the selection of timezones available in the timezone dropdown changes to contain only the timezones present in that country.
Profile Settings
This is the profile settings page, which allows users to change their profile picture, display name, username, and email.
The video demonstrates how each of the boxes were programmed to handle text input.
For all three input boxes, if the field is cleared out entirely, then it resets back to its previous value
For the email input box, any time the user inputs a space, it is automatically deleted.
The email box also only accepts inputs in the form of [name]@[domain].[tld].
If the input deviates from that form, it automatically restores the value to its previous valid value.
Notifications Settings
This is the notifications screen, in which users can customize their notification preferences by clicking on the corresponding switches.
SND refers to the music aspect of the app.
SPLY refers to "supply," which is the retail aspect of the app.
NEWS refers to the mini news article section in the app.
HQ refers to the section of the app dedicated to updates and any other related information.
Security Settings
This is the security screen, where users can change their passwords, reset their forgotten passwords, and deactivate their accounts.
The video on the left demonstrates how the program responds to different combinations of inputs passed into the boxes.
If the password is less than 8 characters long, the field is cleared and the user is prompted to type in a new password.
If the current password is correct and the two new password entries match, the fields are cleared and the user is told that the password was successfully changed.
If the current password is incorrect, even if both new password entries match, the user is told that their password is incorrect and they must try again.
If the current password is correct and the two new entries do not match, the user is told that the passwords do not match.
If multiple of these errors occur at once, all of them are displayed for the user to see.
The 2022 - 2023 FTC Robotics season was the season in which I had the opportunity to do the most work on the robot, as I was the team's lead programmer.
The prior season I was a regular programmer whose main objective was to learn, and the following season I was a mentor whose main objective was to teach the younger team members.
My role as lead programmer was to design the robot and use Java to program it.
The video below demonstrates our robot's functionality in two parts:
The first part is the autonomous portion, in which the robot relies purely on our programming to score 6 cones
It uses color sensors to detect the cones and odometry wheels to estimate its position relative to the starting point.
The second part is the driver-controlled portion, in which we use controllers to control the robot.