程序的机器级表示

程序的机器级表示

有关CSAPP第三章一些我关注到的重点的记录

操作指令

.c->.exe的流程

image-20231127125503391

1.选项 -E : 预编译过程,处理宏定义和include,并作语法检查

gcc -E hello.c -o hello.i              #将hello.c预处理输出为hello.i文件

2.选项 -S : 编译过程,生成通用的汇编代码

gcc -S hello.c                         #生成汇编代码hello.s

生成的汇编文件以“.”开头的行都是指导汇编器和链接器工作的伪指令

3.选项 -c : 汇编过程,生成ELF格式的可重定位目标文件,目标文件(机器代码),用文本编辑器打开是乱码

gcc -c hello.c                         #生成目标代码hello.o(中间文件),不能执行,在Makefile中应用广泛

4.选项 -L : 链接过程,将.o文件与所需库文件链接合并成ELF格式的可执行目标文件,分静态链接和动态链接

gcc hello.o -L dir(如./lib)            #指定库搜索路径,有多个则从前往后搜索

5.选项 -l : 链接过程,指定链接库,库命名规则是libxxx.a,指定库名时使用的格式是-lxxx

gcc hello.c -o hello -lm              #链接数学库
ld -o hello hello.o -lxxx             #链接xxx库

6.选项 -o : 将源文件预处理、编译、汇编并链接形成可执行目标文件,-o选项指定可执行文件的文件名,加载到内存中即可执行

gcc hello.c -o hello                  #生成可执行文件hello

7.部分选项 :
选项 -Wall : 编译时打开警告信息开关
选项 -D : 在文件中定义宏INFO,编译时加上-D INFO使其生效
选项 -O : 后指定数字,使用编译优化级别1~3优化程序
选项 -g : 产生调试信息

8.选项 -static : 使用静态链接库,将使用的静态库对象嵌入至可执行映像文件中,加载时无需进一步的链接

gcc -c -Wall x1.c x2.c       #生成目标文件
ar -cru libxxx.a x1.o x2.o   #创建静态库
#定义静态库的应用接口xxx.h,里面显式引用上面的源文件函数和对象
gcc -O2 -c main.c            #测试用例调用静态库的函数
gcc -static -o p main.o ./libxxx.a  #链接静态库和目标文件生成可执行文件p

9.选项 -share : 使用共享库,在运行时动态加载目标程序所需要的信息
选项 -fPIC : 指示编译器生成与地址无关的目标文件(position-independent code)

gcc -shared -fPIC -o libxxx.so x1.c x2.c  #生成共享库libvector.so
gcc -o p1 main.c ./libvector.so           #共享库中的目标对象并未嵌入可执行文件中,执行时完成链接过程

.c->.exe

linux> gcc -Og -o p -g p.c
  • -Og优化等级比较符合原始C代码整体结构,方便学习(为了更高的性能可以使用-O1或-O2甚至更高的编译优化选项)
  • -o转化成可执行文件
  • -g生成调试信息
  • p为转化成可执行文件的文件名
  • p.c为源文件名

.c->.s 编译生成汇编文件

linux> gcc -Og -S p.c

.c->.o 汇编生成目标文件

linux> gcc -Og -c p.c

.o/.exe->.s 反汇编

linux> objdump -d p.o

C语言嵌套汇编语言

C编译器在把程序中表达的计算转换到机器代码中表现很出色,但仍然有一些及其特性是C语言访问不到的。例如x86-64处理器执行算术或逻辑运算时,修改奇偶标志位寄存器PF的值时,用汇编语言的效率远高于C语言,故如果能在C语言中嵌套C语言,会提供大大的方便。

第一种方法:源代码中插入汇编代码

#include <stdio.h>
#include <stdlib.h>int main(void)
{/* basic command demo */__asm__("movl %eax, %ecx");/* set b = 10 */int a = 10, b = 0;__asm__("movl %1, %%eax;""movl %%eax, %0;":"=r" (b)	/* output */:"r" (a)	/* input */:"%eax"	/* clobbered register */);printf("%s: b = %d\n", __func__, b);return 0;
}

第二种方法:写好汇编文件和C文件,用汇编器和链接器把它们合并起来

保存寄存器

假设现在有两个函数funcA和funcB,函数A称为调用者,函数B称为被调用者,由于调用了函数B,寄存器rbx在函数B中被修改了,而逻辑上rbx寄存器的内容在调用函数B的前后应该保持一致,解决这个问题有两个策略,调用者保存和被调用者保存。

func_A:...movq $123, %rbxcall func_Badd %rbx, %rax...ret
func_B:...addq $456, %rbx...rer

调用者保存

func_A:...movq $123, %rbx保存rbxcall func_B恢复rbxadd %rbx, %rax...ret
func_B:...addq $456, %rbx...rer

被调用者保存

func_A:...movq $123, %rbxcall func_Badd %rbx, %rax...ret
func_B:...保存rbxaddq $456, %rbx恢复rbx...rer

具体使用哪种策略取决于寄存器被定义为那种类型,下图是寄存器类型

image-20231119162344823

c语言基本类型对应汇编后缀表示

image-20231119162527234

访问信息

各存储部件的性价比

image-20231119162939434

通用寄存器
寄存器用途
%eax操作数运算
%ebx指向DS段中数据的指针
%ecx字符串操作和循环计数器
%edx输入输出指针
%esi指向DS段中数据的指针或字符串操作中字符串的复制源
%edi指向ES段中数据的指针或字符串操作中字符串的复制地
%esp栈指针(SS段)
%ebp指向SS段上数据的指针
段寄存器
寄存器用途
CS代码段
DS数据段
SS堆栈段
ES数据段
FS数据段
GS数据段

C类型长度

C声明Intel数据类型汇编代码后缀大小(字节)
char字节b1
shortw2
int双字l4
long四字q8
char*四字q8
float单精度s4
double双精度l8

指令

指令包含操作码和操作数。

	操作码		操作数movq	 (%rdi), %raxaddq	 $8,	%rsxsubq	 %rdi,	%raxxorq	 %rsi,	%rdiret

操作码决定CPU执行操作的类型

指令可以有一个、多个或没有操作数

操作数分为3类,分别为立即数、寄存器以及内存引用

数据寻址模式

image-20231127125528494

这里的比例因子s会根据数据类型取

数据传送命令

image-20231127125732051

以上命令中没有movzlq,是因为一个结论:当复制和生成字节以寄存器为目标时,对于生成4字节的指令,会把高位4个字节置为0,所以用movl就能代替命令movzlq,例如

movl %eax,%edx

实际上除了将低32位数据由eax传递给rdx的低32位之外,还把高32位设置为0

这里注意到练习题3.3的一题,找以下代码的错误

movl %eax,%rdx

在这里错误是源操作数和目标操作数类型不匹配,虽然eax传值后会扩展为64位,但在写代码时依然需要保持操作数类型的统一

movq指令的限制:

当movq指令的源操作数是立即数时,只能是32位的立即数,此时会对该立即数进行符号扩展到64位,再将得到的64位立即数传送到目的位置。

那么当源操作数是64位立即数时就引入了一个新的指令movabsq,此时就能将64位立即数作为源操作数,但目的操作数只能是寄存器

cltq指令

cltq = movslq %eax,%rax

算术和逻辑操作

操作指令

image-20231127125536013

具体操作如下图,之所以z被分为两步操作,是因为比例因子只能取1、2、4、8这四个数中的一个

image-20231120193534676

移位操作

移位量可以是一个立即数,或者放在单字节寄存器%cl中。

移位操作对w位长的数据值进行操作,移位量是由**%cl寄存器**的低m位决定的,这里2m=w,高位被忽略。所以,例如寄存器%cl的十六进制值为0xFF时,指令salb会移7位,salw会移15位,sall会移31位,而salq会移63位。

SAR算术右移,高位补符号位;SHR逻辑右移,高位补0;

以下操作使用移位操作而不使用乘法操作的原因是因为乘法指令执行需要更长时间,因此编译器在生成汇编指令时,会优先考虑更高效的方式。

image-20231120194527867

特殊的算术操作

image-20231127125544066

控制

条件码

CPU除了提供上面的几个整数寄存器外,还维护着一组单个比特位的条件码,描述最近的算术或逻辑操作特性,用于执行条件分支指令。

  • CF: 进位标志,表示最近的操作使最高位产生了进位。用于检查无符号操作数的溢出,如下图image-20231120195141469

  • ZF: 零标志,表示最近的操作得出的结果为0,如下图

    image-20231120195223613

  • SF: 符号标志,表示最近的操作得出的结果为负数

  • OF: 溢出标志,表示最近的操作使补码溢出-正溢出或负溢出

条件码寄存器的值是由ALU执行算术逻辑运算指令改变的

有几种设置条件码的情形

image-20231120195517929

INC(加一)和DEC(减一)指令会设置OF(溢出)标志和ZF(零)标志,但不会改变CF(进位)标志。

因为指令系统设计人员考虑该指令主要用于对指针(即地址)进行增加,不存在进位问题,所以没有设计让INC影响进位标志CF。
INC,DEC指令不影响CF标志位,这个是Intel规定的!其原因是硬件设计造成的,总之,对软件人员来制说不重要!
INC,DEC指令不影响CF标志位,这表明执行INC/DEC指令之后,CF不能反映进位情况。

INC 0000000011111111

0000000011111111+1当然要进位,但不设置CF为1。
我们的问题就在于,将进位与CF等同
CF被称为进位标志位,在多数情况下,它确实反映进位情况,但不是绝对的,INC/DEC就是其中两例
INC/DEC指令不影响CF标志位,这句话就是明明白白地告诉你,此时,CF与进位无关

A. 比较和测试指令:它们只设置条件码而不改变任何其他寄存器

cmp S2,S1 通过S1-S2的结果,比较两者的大小
test S2,S1 通过S1&S2的结果(按位与),比如testl %eax,%eax用来检查%eax是正数,负数还是0或者其中一个操作数是掩码,用来指示哪些位应该被测试

B. 根据条件码的组合,使用set指令,不同后缀名表示不同条件

set指令的目的操作数是8个单字节寄存器或者存储一个字节的存储器位置,把该字节位置设置成0或1。它的基本思路是执行比较或测试指令,根据set指令的类型决定计算结果t=a-b:操作数的大小,是有符号的还是无符号的,程序值的数据类型。如图所示为set指令的常见情形

image-20231127125554540

跳转指令

image-20231127125601201

关于跳转指令如何编码

image-20231127125810240

可以看到第2行中跳转指令目标指明位0x8,第5行中跳转指令跳转目标是0x5,这里有一个规则,在指令的字节编码中,我们可以看到第二个字节中编码位0x3,再将其加上下一条指令的地址,即0x5,就可以得到跳转目标地址0x8,同样第5行0xf8(即十进制-8),这个数加上0xd,即为地址0x5

条件分支

用条件控制来实现条件分支
实际上,C语言中有一种语句叫做goto,一般不推荐使用,但是它的控制和汇编代码的条件转移十分相似。

例如我们有这样一段正常的代码,实际上就是得到两数之差的绝对值:

long absdiff(long x, long y)
{
long result;
if (x > y)
result = x-y;
else
result = y-x;
return result;
}

然后我们使用goto语句改写一下:

long absdiff_j(long x, long y)
{
long result;
int ntest = x <= y;
if (ntest) goto Else;
result = x-y;
goto Done;
Else:
result = y-x;
Done:
return result;
}

从控制流的角度来看,这两个代码基本上是一样的。

用条件传送来实现条件分支
条件传送,和set指令有些相似,也就是根据条件码部分来判断是否要进行数据传送,使用的是cmov(conditional move),比如当相等的时候进行条件传送,也就是cmove。

现代处理器会使用一种特殊的技术,叫做流水线(pipeline),它的名字就是取自工厂流水线,在CPU中,也就是说当你执行一条指令的时候,下一条指令的一部分会被执行,下下一条指令的一部分也会被执行,这样就提高了并行的程序。

但是条件转移会破坏流水线的运作,于是我们会把两个条件的结果都计算一遍,然后再根据跳转选择其中的一条。这里也就用到了cmov。

比如还是之前的程序,我们汇编变成如下这个样子,也就是把x-y和y-x都计算了,然后再根据条件,选择其中一个结果返回:

absdiff:
movq %rdi, %rax # x
subq %rsi, %rax # result = x-y
movq %rsi, %rdx
subq %rdi, %rdx # eval = y-x
cmpq %rsi, %rdi # x:y
cmovle %rdx, %rax # if <=, result = eval
ret
但是,使用cmov也会有一些负面影响:

只有当计算较为简单时,才用cmov进行优化,如果两条分支都较为复杂,那么使用cmov反而不好
对于某个分支而言,计算它可能没有什么用,只是浪费时间。
两个分支可能会存在关联性,比如val = x > 0 ? x*=7 : x+=3;,如果两个都进行计算就会出现错误。

指令同义名传送条件描述
cmove S,RcmovzZF相等/零
cmovne S,Rcmovnz~ZF不相等/非零
cmovs S,RSF负数
cmovns S,R~SF非负数
cmovg S,Rcmovnle~(SF^OF) & ~ZF大于(有符号>)
cmovge S,Rcmovnl~(SF^OF)大于或等于(有符号>=)
cmovl S,RcmovngeSF^OF小于(有符号<)
cmovle S,Rcmovng(SF^OF) | ZF小于或等于(有符号<=)
cmova S,Rcmovnbe~CF & ~ZF超过(无符号>)
cmovae S,Rcmovnb~CF超过或相等(无符号>=)
cmovb S,RcmovnaeCF低于(无符号<)
cmovbe S,RcmovnaCF | ZF低于或相等(无符号<=)

练习题3.20

在这里发现一个规则,当负数做被除数时,需要将该数先加上2k-1,k为要右移的位数。这是为了保证,正数向下舍入,负数向上舍入

image-20231127125626439

循环

一、do-while

如果用C的goto来实现,则如下面的代码:

loop:Bodyif (Test)goto loop

实际上就是先循环体,然后进行测试,如果测试成功,那么跳回到loop再继续循环。

举个例子,比如我们有这样一个C程序的goto版本:

long pcount_goto (unsigned long x) {long result = 0;loop:result += x & 0x1;x >>= 1;if(x) goto loop;return result;
}

那么会发现,汇编的版本也类似:

movl    $0, %eax		#  result = 0
.L2:			# loop:movq    %rdi, %rdx	andl    $1, %edx		#  t = x & 0x1addq    %rdx, %rax	#  result += tshrq    %rdi		#  x >>= 1jne     .L2		#  if (x) goto looprep; ret

二、while

while和do-while的区别就在于do-while第一次不进行测试,所以总会执行一遍循环体,而while在开始就测试,如果不满足就跳出,不执行。

while的实现有2种方式,第一种方式就是先跳到了do-while的中间,然后进行测试。

C的goto版本如下:

goto test;
loop:Body
test:if (Test)goto loop;
done:

我们还是用pcount这个程序,那么while就是下面这种实现:

long pcount_goto_jtm(unsigned long x) {long result = 0;goto test;loop:result += x & 0x1;x >>= 1;test:if(x) goto loop;return result;
}

第二种实现方式比较传统,就是一开始进行判断,如果不满足直接goto跳出,满足那么进入到和do-while相同的语句块中。

 if (!Test)goto done;
loop:Bodyif (Test)goto loop;
done:

pcount的第二种while实现如下:

long pcount_goto_dw(unsigned long x) {long result = 0;if (!x) goto done;loop:result += x & 0x1;x >>= 1;if(x) goto loop;done:return result;
}

三、for

for循环实际上包含了4个部分,例如一个C语言的for循环for(int i = 0;i < 5;i++){body},包括了初始化(int i = 0),测试(i < 5),更新(i++)和循环体。

如果用while循环来表示for循环,那么就是先进行初始化,然后是while循环,在while循环体的最后加上更新操作。

Init;
while (Test ) {BodyUpdate;
}

还是之前的例子,我们使用for循环(用while实现for)实现:

long pcount_for_while(unsigned long x)
{size_t i;long result = 0;i = 0;while (i < WSIZE){unsigned bit = (x >> i) & 0x1;result += bit;i++;}return result;
}

然后我们用goto替代:

long pcount_for_goto_dw(unsigned long x) {size_t i;long result = 0;i = 0;if (!(i < WSIZE))goto done;loop:{unsigned bit = (x >> i) & 0x1;result += bit;}i++;if (i < WSIZE)goto loop;done:return result;
}

如果使用了-O1优化级别,那么第一次的判断很有可能不需要了,编译器会将其舍弃

这里需要提示一下,再跳转指令后若跟随ret会出现一些判断问题,所以我们需要在中间加一个rep;这个什么都不会做,所以也不需要管

过程

栈帧

当函数执行所需要的存储空间超出寄存器能够存放的大小时,就会借助栈上的存储空间,这部分存储空间称为函数的栈帧

image-20231120202419343

如果一个函数的参数数量大于6,超出部分就要使用栈来传递。

image-20231120202519873

两点注意

1.通过栈传递参数时,所有数据大小向8对齐

image-20231120202751702

2.使用寄存器进行参数传递时,寄存器的使用是由特殊顺序规定的

image-20231120202835825

局部变量在栈帧存储不需要对齐,参数才需要对齐

image-20231120203257254

数组

不同类型指针加1,得到结果不同

image-20231120203818045

数组元素的计算

image-20231120204106003

xd表示数组的起始地址,L表示数组类型T的大小,如果T时int类型,L就等于4,T是char类型,L就等于1

例如下图

image-20231120204236228

使用以下汇编代码,将A[i][j]的值复制到寄存器eax中,如下图所示

image-20231120204338191

结构体

结构体在内存中的存储遵循内存对齐,如下图,由于变量j是int类型,占4个字节,它的起始地址必须是4的倍数,所以,在变量c和变量j之间插入了一个3字节的间隙,结构体大小也就变成了12个字节。

image-20231120205126457

如果我们变更顺序,如下图,此时能满足结构体的对齐要求,但无法满足结构体数组的对齐要求,所以如果定义结构体数组,需要在末端加入3个字节的间隔。

image-20231120205242216

**复杂示例:**每个元素的偏移地址都必须是它数据大小的倍数,且为满足每个元素都对齐,最后要在结构体末端填充间隙(根据结构体最大类型的长度),如下图所示。

image-20231120205456633

联合体

联合体中所有字段共享同一存储区域,因此联合体的大小取决于它最大字段的大小,如下图,变量v和数组i的大小都是8个字节,因此该联合体占8个字节的存储空间,两个不同字段的使用是互斥的,那么我们就可以将这两个字段声明为一个联合体。

image-20231120205849564

示例:一个二叉树,包含叶子节点(只包含两个double数据)和内部节点(只包含左右节点指针),其定义如下图,使用结构体定义需要占用32个字节,而使用联合体只用占用16个字节。

image-20231120210147693

但此时有一个问题,就是无法确定节点是哪种节点,解决办法是引入一个枚举类型,如下图所示,type占4个字节(枚举占4个字节),加上最后末尾间隔的4个字节,最终这个结构体占24个字节

image-20231120210512788

**类型转换:**一种类型来存储,另一种类型来访问

image-20231120210926653

栈溢出攻击

解决通过栈溢出攻击系统的三种办法

1.栈随机化

栈的位置在程序每次运行时都发生变化,在Linux系统中,栈随机化已经成为了一种标准行为(ASLR)

2.栈破坏检测

编译器会在产生的汇编代码中加入一种栈保护者的机制来检测缓冲区越界,就是在缓冲区与栈保存的状态值之间存储一个特殊值(金丝雀值,canary),函数返回之前检测金丝雀值是否被修改来判断是否遭受攻击

image-20231121005508305

3.限制可执行代码区域

这三种机制都不需要程序员做额外的操作,都是通过编译器和操作系统实现的,单独每一种机制都能降低用户的等级,组合起来使用会更有效,不幸的是,仍然有方法能对计算机进行攻击。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/221978.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

BUUCTF刷题之路--ciscn_2019_es_21

这题考察的是一个栈迁移的知识。作为入门学习栈迁移是个不可多得的好题。程序简单并且是32位的架构。保护也没有开&#xff0c;因此对于理解栈迁移再好不过了。看一下这题的基本信息&#xff1a; 栈迁移的基本原理其实就是栈的空间不够我们利用。也就是不不足以覆盖返回地址&am…

Linux 网络通信

(一)套接字Socket概念 Socket 中文意思是“插座”&#xff0c;在 Linux 环境下&#xff0c;用于表示进程 x 间网络通信的特殊文件 类型。本质为内核借助缓冲区形成的伪文件。 既然是文件&#xff0c;那么理所当然的&#xff0c;我们可以使用文件描述符引用套接字。Linux 系统…

【视觉SLAM十四讲学习笔记】第三讲——旋转向量和欧拉角

专栏系列文章如下&#xff1a; 【视觉SLAM十四讲学习笔记】第一讲——SLAM介绍 【视觉SLAM十四讲学习笔记】第二讲——初识SLAM 【视觉SLAM十四讲学习笔记】第三讲——旋转矩阵 【视觉SLAM十四讲学习笔记】第三讲——Eigen库 本章将介绍视觉SLAM的基本问题之一&#xff1a;如何…

物理层之奈氏准则和香农定理

学习的最大理由是想摆脱平庸&#xff0c;早一天就多一份人生的精彩&#xff1b;迟一天就多一天平庸的困扰。各位小伙伴&#xff0c;如果您&#xff1a; 想系统/深入学习某技术知识点… 一个人摸索学习很难坚持&#xff0c;想组团高效学习… 想写博客但无从下手&#xff0c;急需…

springboot核心原理之@SpringbootApplication

1.SpringbootApplication Configuration标志的类 在spring ioc启动的时候就会加载创建这个类对象 EnableAutoConfiguration 中有两个注解 &#xff08;1&#xff09;AutoConfigurationPackage 扫描主程序包(主程序main所在包及其子包) 可以看到这个类 &#xff1a; static c…

React中通过children prop或者React.memo来优化子组件渲染【react性能优化】

文章目录 前言未优化之前的代码问题解决方案一&#xff0c;通过children prop解决方案二&#xff0c;通过React.memo后言 前言 hello world欢迎来到前端的新世界 &#x1f61c;当前文章系列专栏&#xff1a;react.js &#x1f431;‍&#x1f453;博主在前端领域还有很多知识和…

探索RockPlus SECS/GEM平台 - 赋能半导体行业设备互联

SECS/GEM协议&#xff0c;全称为半导体设备通讯标准/通用设备模型&#xff08;SECS/Generic Equipment Model&#xff09;&#xff0c;是一种广泛应用于半导体制造行业的通信协议。它定义了半导体设备与工厂主控系统&#xff08;如MES&#xff09;之间的通信方式&#xff0c;使…

ZYNQ PL 中断请求

1 中断概念 中断学习 2 ZYNQ 中断框图 上图为 zynq 中断分布框图。可以看到部分 PL 到 PS 部分的中断&#xff0c;经过中断控制分配器&#xff08;ICD&#xff09;&#xff0c; 同时进入 CPU1 和 CPU0。查询下面表格&#xff0c;可以看到 PL 到 PS 部分一共有 20 个中断可以使…

长沙市中小学入学报名流程及上传证件照电子版制作方法

长沙市中小学入学报名是家长和学生迈向教育之门的第一步。通常&#xff0c;报名过程分为线上和线下两个阶段。首先&#xff0c;家长需在规定时间内登录报名系统&#xff0c;填写详细的入学信息等。长沙市注重教育公平&#xff0c;为确保每个孩子都有平等的入学机会&#xff0c;…

NX二次开发UF_CURVE_ask_parameterization 函数介绍

文章作者&#xff1a;里海 来源网站&#xff1a;https://blog.csdn.net/WangPaiFeiXingYuan UF_CURVE_ask_parameterization Defined in: uf_curve.h int UF_CURVE_ask_parameterization(tag_t object, double param_range [ 2 ] , int * periodicity ) overview 概述 Retu…

饮料行业用什么ERP软件好

食品饮料产品的安全问题是近些年备受消费者和企业关注的问题&#xff0c;而不同的食品又有差异化的配方、原材料、制造工序和销售渠道与策略等。 近些年饮料企业数量不断增多&#xff0c;由此也导致饮料行业竞争比较激烈&#xff0c;传统的管理模式难以满足现代饮料市场管理需…

解密Spring Cloud微服务调用:如何轻松获取请求目标方的IP和端口

公众号「架构成长指南」&#xff0c;专注于生产实践、云原生、分布式系统、大数据技术分享。 目的 Spring Cloud 线上微服务实例都是2个起步&#xff0c;如果出问题后&#xff0c;在没有ELK等日志分析平台&#xff0c;如何确定调用到了目标服务的那个实例&#xff0c;以此来排…