|
|
1.1 ! root 1: / $Header: /z/vlad/boot/src/RCS/boot.s,v 1.4 91/08/27 14:19:59 vlad Exp Locker: vlad $ ! 2: / (lgl- ! 3: / The information contained herein is a trade secret of Mark Williams ! 4: / Company, and is confidential information. It is provided under a ! 5: / license agreement, and may be copied or disclosed only under the ! 6: / terms of that agreement. Any reproduction or disclosure of this ! 7: / material without the express written authorization of Mark Williams ! 8: / Company or persuant to the license agreement is unlawful. ! 9: / ! 10: / COHERENT Version 2.3.37 ! 11: / Copyright (c) 1982, 1983, 1984. ! 12: / An unpublished work by Mark Williams Company, Chicago. ! 13: / All rights reserved. ! 14: / -lgl) ! 15: ! 16: //////// ! 17: / ! 18: / Bootstrap block for the IBM ! 19: / PC. This code, which lives in sector 1, ! 20: / track 0 of the floppy (or sector 1 of a hard disk partition) ! 21: / gets read into location 0x7C00 by the ROM. ! 22: / The code relocates itself to location 0x2060:0x0000 ! 23: / (128K above the fixed memory used by the ROM). The system image is read ! 24: / in beginning at paragraph 0x0060. ! 25: / ! 26: / $Log: boot.s,v $ ! 27: /RCS-> Revision 1.4 91/08/27 14:19:59 vlad ! 28: /RCS-> Fixed boot that allow to boot from arbitrary place of the disk, by ! 29: /RCS-> using four byte block numbers. Original (oldest and Ciarian) used ! 30: /RCS-> only two byte block numbers -- only the first 32 Meg were accessable. ! 31: /RCS-> ! 32: / Revision 1.1 88/03/24 16:44:09 src ! 33: / Initial revision ! 34: / ! 35: / 87/10/04 Allan Cornish /usr/src/sys/i8086/boot/boot.s ! 36: / Number of heads now defined by NHD macro. Should be 1 for f*d*, 2 for f*a*. ! 37: / ! 38: / 85/10/04 Allan Cornish /usr/src/sys/i8086/boot/boot.s ! 39: / Bootstrap extended to increase max file size from 64 Kbytes to 128 Kbytes. ! 40: / The private code segment is now loaded, where previously it had to be empty. ! 41: / File name editing enhanced to help cursor stay in the 14 char entry field, ! 42: / and to provide a destructive backspace. ! 43: / ! 44: / 85/01/04 Allan Cornish ! 45: / Added backspace editing of file name. ! 46: / ! 47: //////// ! 48: ! 49: #ifndef NHD ! 50: #define NHD 1 / # of heads per drive [1 for f9d0]. ! 51: #endif ! 52: ! 53: #ifndef NSPT ! 54: #define NSPT 9 / # of sectors per track on floppy. ! 55: #define NTRK 40 / # of tracks on floppy. ! 56: #endif ! 57: ! 58: NSTK = 256 / # of bytes of stack. ! 59: BS = 0x08 / BS character. ! 60: CR = 0x0D / CR character. ! 61: LF = 0x0A / LF character. ! 62: SP = 0x20 / Backspace character. ! 63: BOOTLC = 0x7C00 / Boot location (ROM). ! 64: BOOTS = 0 / boot segment (ROM). ! 65: RBOOTS = 0x2060 / Relocated boot segment. ! 66: BSHIFT = 9 / Shift, bytes to blocks. ! 67: ROOTINO = 2 / Root inode # ! 68: INOORG = 2 / First inode block. ! 69: IBSHIFT = 3 / Shift, inode to blocks ! 70: IOSHIFT = 6 / Shift, inode to bytes ! 71: INOMASK = 0x0007 / Mask, inode to offset ! 72: DIRSIZE = 14 / Directory size. ! 73: DIRSIZ2 = 7 / Directory size / 2. ! 74: BUFSIZE = 512 / Block size. ! 75: HDRSIZE = 44 / L.out header size. ! 76: HDRSIZ2 = 22 / L.out header size / 2. ! 77: BUFMSK2 = 0x00FF / Block [word] mask, for reads. ! 78: DISK = 0x13 / Disk Interrupt ! 79: KEYBD = 0x16 / Keyboard Interrupt ! 80: READ1 = 0x0201 / read one sector ! 81: DADDR = 4 / number of bytes in a disk block number ! 82: ND = 10 / # of direct blocks. ! 83: NIND = 128 / # of blocks in indirect block. ! 84: NINDX2 = 256 / NIND * 2 (number of words in indirect) ! 85: ISIZE = 8 / Offset of "di_size". ! 86: IADDR = 12 / Offset of "di_addr". ! 87: JMPF = 0xEA / Jump far, direct. ! 88: LMAGIC = 0 / Offset of "l_magic" ! 89: LFLAG = 2 / Offset of "l_flag" ! 90: LSHRI = 10 / Offset of "l_ssize[SHRI]" ! 91: LPRVI = 14 / Offset of "l_ssize[PRVI]" ! 92: LSHRD = 22 / Offset of "l_ssize[SHRD]" ! 93: LPRVD = 26 / Offset of "l_ssize[PRVD]" ! 94: LBSSD = 30 / Offset of "l_ssize[BSSD]" ! 95: LFMAG = 0x0107 / Magic number. ! 96: LFSEP = 0x02 / Sep I/D flag bit. ! 97: SYSBASE = 0x0060 / System load base paragraph. ! 98: FIRST = 8 / relative start of partition ! 99: ! 100: //////// ! 101: / Overall structure of boot.s (the secondary boot): ! 102: / ! 103: / boot: relocate secondary to high memory (0x2060:0x0000) ! 104: / if needed, get hard disk parameters from bios ! 105: / jump far to high memory (RBOOTS:entry) ! 106: / entry: open directory "/" ! 107: / search: while filename not found ! 108: / walk through "/" looking for filename (in nbuf) ! 109: / if not found, ask for another name (call input:), and loop to search ! 110: / found: open file that we just found ! 111: / read the l.out header (verify magic number, get partition sizes) ! 112: / load the shared and private code segments as one (call load:) ! 113: / load the initialized data segment (call load:) ! 114: / jump far to the newly loaded program ! 115: / ! 116: //////// ! 117: ! 118: //////// ! 119: / ! 120: / Bootstrap mainline. Relocate the ! 121: / boot to high memory, so it won't be written ! 122: / over by the system. Read in a file name. Look up ! 123: / the file name by following out the directory ! 124: / structure. Read in the image and jump to it. ! 125: / ! 126: //////// ! 127: ! 128: boot: xor di, di ! 129: mov ds, di ! 130: mov si, $BOOTLC / Make DS:SI point at the code ! 131: / "mov es, 1f(si)" really means "mov es, $RBOOTS". Clever Intel... :-( ! 132: mov es, 1f(si) / Make ES:DI point at where it goes ! 133: mov cx, $512 / cx = # of bytes to move ! 134: cld ! 135: rep / Move the bootstrap ! 136: movsb / to high memory. ! 137: ! 138: / Check to see if we were invoked by mboot. (That means we're booting ! 139: / off a hard disk and need to load disk parameters. ! 140: ! 141: / mboot sets bx = sp ! 142: cmp bx, sp / trick to tell ! 143: jne 0f / whether or not xt ! 144: ! 145: / Assertion: at this point we know we're boot off a hard disk. ! 146: mov si, bp / set si to partition table ! 147: ! 148: movb dl, (si) / get drive number ! 149: movb ah, $8 / get drive parameters ! 150: int DISK ! 151: jc 0f / abort on error ! 152: ! 153: movb al, ch / fetch cyl(lo) ! 154: movb ah, cl / move cyl(hi), sects ! 155: rolb ah, $1 / shift cylinder high to ! 156: rolb ah, $1 / the least sig bits ! 157: andb ah, $3 / mask out cylinder bits ! 158: ! 159: mov di, $traks / point to drive ! 160: stosw / set number of tracks ! 161: ! 162: movb al, $0x3F / sector mask ! 163: andb al,cl / mask sector ! 164: stosb / set sector ! 165: ! 166: movb al, dh / get max head ! 167: inc ax / change to # of heads [incb al] ! 168: stosb / set number of heads ! 169: ! 170: movsb / set drive ! 171: add si, $FIRST-1 / point to first block ! 172: movsw ! 173: movsw / fetch first block ! 174: ! 175: / Set registers. ! 176: ! 177: 0: mov bp, $stack+NSTK / stack pointer value ! 178: mov ax, es ! 179: mov ds, ax ! 180: mov ss, ax ! 181: mov sp, bp ! 182: ! 183: call login / print signon message ! 184: ! 185: / Jump to (relocated) boot. ! 186: ! 187: .byte JMPF ! 188: .word entry ! 189: 1: .word RBOOTS ! 190: ! 191: //////// ! 192: / ! 193: / This is the equivalent of "open()", ! 194: / but takes an inode instead of a pathname. ! 195: / ! 196: / Read the inode specified by "ax" ! 197: / into the external variable "iaddr". ! 198: / ! 199: / Makes a list of block numbers in "iaddr" ! 200: / consisting of the 10 direct block numbers, the 128 ! 201: / block numbers pointed to the singly indirect block, ! 202: / and the first 128 block numbers pointed to by the ! 203: / first doubly indirect block. ! 204: / ! 205: / Note that "iread" can only access the first 256 ! 206: / block numbers in this list, so the last 10 are ! 207: / ignored. ! 208: / ! 209: / Note that block numbers are actually 32 bits on disk-- ! 210: / this routine only uses the lower 16 bits. ! 211: / In fact, iaddr is loaded with 16 bit numbers, not 32 bit. ! 212: / ! 213: / No checking is done to make sure that ! 214: / the inumber is in range on the filesystem. ! 215: / Sets dx and cx to 0. ! 216: / Mungs si, di, ax. ! 217: / ! 218: //////// ! 219: ! 220: igrab: dec ax / Make origin 0 and ! 221: push ax / remember for later use. ! 222: ! 223: / We assume that the inode table appears in the first 32 meg of disk. ! 224: / Look up the inode in the inode table. ! 225: movb cl, $IBSHIFT / Convert to ! 226: shr ax, cl / inode block number, ! 227: inc ax / then to physical block number, ! 228: inc ax ! 229: sub bx, bx / Assume inode within first 32 meg. ! 230: call bread / and read in the data. ! 231: ! 232: pop ax / Get i-number back. ! 233: and ax, $INOMASK / Get inode within block ! 234: movb cl, $IOSHIFT / and convert to ! 235: shl ax, cl / a byte offset in the block. ! 236: add si, ax / si = inode pointer. ! 237: ! 238: / Assertion: si now points at the start of the desired ! 239: / inode structure (see <sys/inode.h>). ! 240: ! 241: / Copy out the direct block numbers to iaddr. ! 242: add si, $IADDR / Point at address field. ! 243: mov di, $iaddr / Copy out area. ! 244: ! 245: / The block numbers in the inode on disk are stored as ! 246: / 3 bytes: msb, lsw. lsw is lsb, msb. We want to store them ! 247: / in memory as msw, lsw, just like the inodes in the indirect ! 248: / blocks on disk. So we need to cast msb as msw. ! 249: movb cl, $ND / cx = # of direct blocks ! 250: sub ax, ax / cast byte in al to word in ax ! 251: 0: lodsb / al = msb for direct byte ! 252: stosw / store msb cast as a word ! 253: movsw / store lsw. ! 254: loop 0b ! 255: ! 256: lodsb / Get the msb for the indirect block. ! 257: xchg bx, ax / (implicit cast of msb to msw) ! 258: lodsw / Get the lsw for the indirect block. ! 259: call bread / Read (first) indirect block. ! 260: ! 261: / Copy out the singly indirect block numbers to iaddr. ! 262: / These are stored on disk as 4 byte numbers: msw, lsw ! 263: / where each word is lsb, msb. This is how we store them ! 264: / in memory. ! 265: mov cx, $NINDX2 ! 266: rep ! 267: movsw ! 268: ! 269: / Finish by reading the first block of the file into bbuf. ! 270: sub dx, dx / Set dh to first block number in iaddr. ! 271: / jmp iread / Done return through iread. ! 272: ! 273: //////// ! 274: / ! 275: / This routine reads the virtual ! 276: / block iaddr[dh] (the dh'th block number ! 277: / in iaddr) into the buffer "bbuf". ! 278: / It uses the mapping data that ! 279: / has been provided by a previous call to igrab ! 280: / On return "si" points at "bbuf". ! 281: / Holes in files (sparse blocks) are correctly done. ! 282: / mungs ax, bx, si. ! 283: / clears cx. ! 284: / ! 285: /////// ! 286: ! 287: iread: ! 288: sub bx, bx / Convert from words to blocks. ! 289: movb bl, dh / ! 290: ! 291: / Block numbers are 4 bytes long, stored msw, lsw. ! 292: shl bx, $2 / Compute index into iaddr table. ! 293: push iaddr(bx) / fetch msw ! 294: inc bx ! 295: inc bx ! 296: mov ax, iaddr(bx) / fetch lsw ! 297: pop bx / put msw where it belongs ! 298: / jmp bread / Yes, return through "bread". ! 299: ! 300: //////// ! 301: / ! 302: / Read a block from the floppy disk, ! 303: / drive A:, using the code in the IBM firmware. ! 304: / The physical block # is in ax and bx. ! 305: / Register bx is the MSW, and ax is the LSW. ! 306: / ! 307: / This code is restricted to reading from the first 32meg of ! 308: / disk because the block number is only 16 bits long. ! 309: //////// ! 310: ! 311: bread: push es / Save registers ! 312: push di ! 313: push dx ! 314: ! 315: push ds ! 316: pop es / set ES to the address of the buffer ! 317: ! 318: mov di, bp / Blast the buffer contents. ! 319: mov cx, $BUFSIZE / For block 0, this fills the buffer ! 320: rep / with zeros. ! 321: stosb ! 322: ! 323: / Block #0 is the sparse block--it means a block of all zeros. ! 324: test ax, ax / if block 0, return zeroed buffer ! 325: jz 2f ! 326: ! 327: / Translate block number into cylinder, head, and sector. ! 328: push bx ! 329: pop dx ! 330: / xor dx, dx / extend block number ! 331: add ax, first / add first block ! 332: adc dx, first+2 / add rest ! 333: ! 334: mov bx, ax / save block number ! 335: movb al, heads / get number of heads ! 336: movb cl, sects / get number of sectors ! 337: mulb cl / calculate sectors per cylinder ! 338: xchg bx,ax / swap block/sectors ! 339: div bx / calculate track ! 340: xchg dx, ax / put track in DX ! 341: divb cl / calculate head/sector ! 342: ! 343: movb cl, ah / set sector ! 344: inc cx / sectors start at 1 [incb cl] ! 345: ! 346: cmp dx, traks / check for second side ! 347: jb 0f ! 348: sub dx, traks / fold track ! 349: inc ax / next head [incb al] ! 350: ! 351: 0: rorb dh, $1 / rotate track(low) into ! 352: rorb dh, $1 / msbits of DX ! 353: orb cl, dh / set track(high) ! 354: movb ch, dl / set track(low) ! 355: movb dh, al / set head ! 356: movb dl, drive / set drive ! 357: mov bx, bp / set offset [bbuf] ! 358: ! 359: mov ax, $READ1 / Read, 1 sector. ! 360: int DISK / Disk I/O. ! 361: jnc 2f / Jump if no error. ! 362: mov ax, $READ1 / try again ! 363: int DISK ! 364: jc error ! 365: ! 366: 2: mov si, bp / set SI to point to buffer for return ! 367: sub cx, cx / clear CX here to save space ! 368: ! 369: pop dx / restore registers. ! 370: pop di ! 371: pop es ! 372: ret / return. ! 373: ! 374: //////// ! 375: / ! 376: / Print signon message ! 377: / ! 378: //////// ! 379: ! 380: login: mov si, $msg00 / point si to message ! 381: ! 382: print: lodsb / al=byte ! 383: cmpb al, $LF / check for end of message ! 384: call putc / Type it and ! 385: jne print / go back for more. ! 386: ret ! 387: ! 388: / Prompt with a "?" and read the file name ! 389: / into "nbuf". No character editing facilites ! 390: / are provided. ! 391: ! 392: error: mov sp, bp / Reset stack ! 393: call login / print boot message ! 394: ! 395: input: mov di, $nbuf / di=name buffer pointer ! 396: mov cx, $DIRSIZE / cx=size of ! 397: movb al, $'? / set prompt ! 398: 0: call putc ! 399: ! 400: 1: movb ah, $0 / Get ASCII opcode. ! 401: int KEYBD / Read keyboard ROM call. ! 402: ! 403: 2: cmpb al, $CR / CR ? ! 404: je 3f / Yup, do next thing ! 405: ! 406: jcxz 1b / skip over too big name ! 407: stosb / put it into the buffer, ! 408: dec cx / adjust char count ! 409: jmp 0b / and continue. ! 410: ! 411: 3: mov si, $crlf / Echo LF followed by CR. ! 412: call print ! 413: ! 414: movb al, $0 / zero out ! 415: rep ! 416: stosb / rest of name ! 417: ! 418: //////// ! 419: / This is where we jump after boot has been relocated. ! 420: / ! 421: / Open "/"; we're going to search for "autoboot". ! 422: //////// ! 423: entry: mov ax, $ROOTINO / ax = current inode ! 424: call igrab / Read in inode and set dx ! 425: ! 426: //////// ! 427: / Search directory for filename in nbuf. ! 428: / Assume directory size = 64 Kbytes. ! 429: / This assumption works because block numbers ! 430: / from the inode beyond end of file are zero. ! 431: //////// ! 432: ! 433: search: orb dl, dl / On block boundry ? ! 434: jnz 0f / Nope. ! 435: call iread / Read block, set si. ! 436: ! 437: 0: movb cl, $DIRSIZE / cx = count left ! 438: mov di, $nbuf / Point at name buffer and ! 439: lodsw / ax = inumber, this entry. ! 440: or ax, ax / Empty directory slot ? ! 441: je 1f / Yes, skip. ! 442: repe / compare the ! 443: cmpsb / two file names. ! 444: je found / If eq, go and get inode. ! 445: ! 446: 1: add si, cx / Advance by count remaining. ! 447: add dx, $DIRSIZ2+1 / Bump [word] seek pointer ! 448: jg search / Scan up to 64 Kbytes of directory. ! 449: ! 450: jmp input / File not found. ! 451: ! 452: //////// ! 453: / ! 454: / Found the file so read it in. ! 455: / ax = inode number of file ! 456: / ! 457: //////// ! 458: ! 459: found: call igrab / read in inode and first block ! 460: cmp LMAGIC(si),$LFMAG / Check the magic number. ! 461: jne error / not Ok. ! 462: ! 463: / push LBSSD(si) / Push the uninitialized data size. ! 464: ! 465: / Get the total size of shared and private data segments. ! 466: / They are contigious on disk. see <l.out.h> ! 467: mov ax, LSHRD(si) / Push the sum ! 468: add ax, LPRVD(si) / of the shared and private ! 469: push ax / [initialized] data sizes. ! 470: / If this is not a sep I/D executable, then we ! 471: / don't want to load the non-existent data segment. ! 472: andb LFLAG(si), $LFSEP / check image flags. ! 473: pushf / Push result of test. ! 474: mov ax, LSHRI(si) / Get the sum of the ! 475: add ax, LPRVI(si) / shared and private code sizes. ! 476: ! 477: / Find the start of the first segment on disk. ! 478: mov dx, $HDRSIZ2 / Seek after header and ! 479: add si, $HDRSIZE / set buffer pointer appropriately. ! 480: mov es, 1f / Set ES:DI to point to the load ! 481: sub di, di / base of the new system. ! 482: ! 483: / Load the code segment. ! 484: call load / Load code. ! 485: ! 486: / Skip loading the data segment for non-sep I/D executables. ! 487: popf / Pop sep I/D flag test ! 488: jz 0f / Not sep. ! 489: ! 490: / Assertion: at this point we are dealing with a sep I/D ! 491: / l.out executable. ! 492: ! 493: / Calculate load point for data segment. ! 494: add di, $15 / Round up the system code ! 495: movb cl, $4 / size to 16 byte ! 496: shr di, cl / paragraphs. ! 497: mov ax, es / fetch program base ! 498: add ax, di / Compute the data base and ! 499: mov es, ax / set up ES:DI to point ! 500: sub di, di / at it. ! 501: ! 502: 0: pop ax / Pop off initialized data size and ! 503: / Load the initialized data segment. ! 504: call load / load the image. ! 505: ! 506: / pop cx / Pop off uninitialized data size and ! 507: / rep / clear it. ! 508: / stosb ! 509: ! 510: / Run the program that we just loaded. ! 511: .byte JMPF / Jump to offset ! 512: .word 0x0100 / 0x0100 (after base) in system ! 513: 1: .word SYSBASE / code segment. ! 514: ! 515: //////// ! 516: / ! 517: / Load a segment from an l.out file into memory. ! 518: / The "dx" register holds the ! 519: / seek pointer into the file. The "si" holds ! 520: / a pointer into the "bbuf". The "es:di" pair ! 521: / holds the target address. The "ax" holds ! 522: / the number of bytes to load. On return the ! 523: / "ax" must equal 0 (the caller assumes this ! 524: / and uses "al" to clear the BSS). ! 525: / ! 526: / Note that since the number of bytes to load is ! 527: / stored in a 16 bit register, a segment may be no ! 528: / longer than 64K. For small model this is just fine. ! 529: //////// ! 530: ! 531: load: or ax, ax / Any left ? ! 532: jz return / Jump if all loaded. ! 533: ! 534: mov cx, $bbuf+BUFSIZE / Compute the number of bytes ! 535: sub cx, si / remaining in the block. ! 536: jnz 0f / Jump if some. ! 537: ! 538: push ax / Save count registers, then ! 539: call iread / read the next block ! 540: pop ax / of the file. ! 541: mov cx, $BUFSIZE / We now have a full block. ! 542: ! 543: 0: cmp cx, ax / More than we need ? ! 544: jbe 1f / Nope. ! 545: mov cx, ax / Only take what we need. ! 546: ! 547: 1: sub ax, cx / Fix up the count ! 548: shr cx, $1 / ! 549: add dx, cx / Fix up the seek [word] address, then ! 550: rep / copy the words from the block ! 551: movsw / buffer to the load point and ! 552: jmp load / loop until done. ! 553: ! 554: //////// ! 555: / ! 556: / Write the character in "al" out to ! 557: / the display, using routines in the ROM. ! 558: / Like most calls to the ROM, this routine spends ! 559: / most of its time saving and restoring the ! 560: / registers. ! 561: / ! 562: / Note: The ZF in the PSW word must be preserved. ! 563: / ! 564: //////// ! 565: ! 566: putc: push si / Save registers. ! 567: push di ! 568: push bp ! 569: ! 570: mov bx, $0x0007 / Page 0, white on black ! 571: movb ah, $0x0E / Write TTY. ! 572: int 0x10 / Call video I/O in ROM. ! 573: ! 574: pop bp / Restore registers. ! 575: pop di ! 576: pop si ! 577: return: ret / Return ! 578: ! 579: //////// ! 580: / ! 581: / Data. There is some pure data in ! 582: / the "prvd" segment. This data is moved to the ! 583: / new memory when the boot is relocated. All buffers ! 584: / are in the BSS, and are not actually moved, or even ! 585: / saved in the boot block. ! 586: / ! 587: //////// ! 588: ! 589: .prvd ! 590: nbuf: .ascii "autoboot" / Name buffer. ! 591: .blkb DIRSIZE-8 / rest of buffer (8=sizeof("autoboot")) ! 592: ! 593: / Defaults for all the following parameters match a floppy disk. ! 594: ! 595: traks: .word NTRK / Number of cylinders on drive we're booting off of. ! 596: sects: .byte NSPT / Number of sectors per track for our drive. ! 597: heads: .byte NHD / Number of heads on drive we're booting off of. ! 598: ! 599: drive: .byte 0 / Drive our partition resides upon. ! 600: first: .word 0 / First block of our partition (?) ! 601: .word 0 ! 602: ! 603: msg00: .ascii "NATboot" ! 604: crlf: .byte CR, LF ! 605: ! 606: / This magic pair of bytes must be the last two bytes of ! 607: / the sector (address 0x1FE), otherwise mboot will refuse ! 608: / to execute it. ! 609: / If needed, uncomment the .blkb and adjust the number appropriately. ! 610: .blkb 0x1c / Padding need to make magic byte line up ! 611: .byte 0x55,0xAA ! 612: ! 613: .bssd ! 614: stack: .blkb NSTK / Local Stack and name buffer ! 615: bbuf: .blkb BUFSIZE / Block buffer [must follow stack]. ! 616: iaddr: .blkw ND+ND+NIND+NIND / Inode map words.
This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.